/* * Copyright 2011 gitblit.com. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.gitblit.models; import java.io.Serializable; import java.security.Principal; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import com.gitblit.Constants; import com.gitblit.Constants.AccessPermission; import com.gitblit.Constants.AccessRestrictionType; import com.gitblit.Constants.AccountType; import com.gitblit.Constants.AuthorizationControl; import com.gitblit.Constants.PermissionType; import com.gitblit.Constants.RegistrantType; import com.gitblit.utils.ArrayUtils; import com.gitblit.utils.ModelUtils; import com.gitblit.utils.StringUtils; /** * UserModel is a serializable model class that represents a user and the user's * restricted repository memberships. Instances of UserModels are also used as * servlet user principals. * * @author James Moger * */ public class UserModel implements Principal, Serializable, Comparable { private static final long serialVersionUID = 1L; public static final UserModel ANONYMOUS = new UserModel(); // field names are reflectively mapped in EditUser page public String username; public String password; public String cookie; public String displayName; public String emailAddress; public String organizationalUnit; public String organization; public String locality; public String stateProvince; public String countryCode; public boolean canAdmin; public boolean canFork; public boolean canCreate; public boolean excludeFromFederation; // retained for backwards-compatibility with RPC clients @Deprecated public final Set repositories = new HashSet(); public final Map permissions = new LinkedHashMap(); public final Set teams = new TreeSet(); // non-persisted fields public boolean isAuthenticated; public AccountType accountType; public UserPreferences userPreferences; public UserModel(String username) { this.username = username; this.isAuthenticated = true; this.accountType = AccountType.LOCAL; this.userPreferences = new UserPreferences(this.username); } private UserModel() { this.username = "$anonymous"; this.isAuthenticated = false; this.accountType = AccountType.LOCAL; this.userPreferences = new UserPreferences(this.username); } public boolean isLocalAccount() { return !Constants.EXTERNAL_ACCOUNT.equals(password) || accountType == null || accountType.isLocal(); } /** * Returns a list of repository permissions for this user exclusive of * permissions inherited from team memberships. * * @return the user's list of permissions */ public List getRepositoryPermissions() { List list = new ArrayList(); if (canAdmin()) { // user has REWIND access to all repositories return list; } for (Map.Entry entry : permissions.entrySet()) { String registrant = entry.getKey(); AccessPermission ap = entry.getValue(); String source = null; boolean mutable = true; PermissionType pType = PermissionType.EXPLICIT; if (isMyPersonalRepository(registrant)) { pType = PermissionType.OWNER; ap = AccessPermission.REWIND; mutable = false; } else if (StringUtils.findInvalidCharacter(registrant) != null) { // a regex will have at least 1 invalid character pType = PermissionType.REGEX; source = registrant; } list.add(new RegistrantAccessPermission(registrant, ap, pType, RegistrantType.REPOSITORY, source, mutable)); } Collections.sort(list); // include immutable team permissions, being careful to preserve order Set set = new LinkedHashSet(list); for (TeamModel team : teams) { for (RegistrantAccessPermission teamPermission : team.getRepositoryPermissions()) { // we can not change an inherited team permission, though we can override teamPermission.registrantType = RegistrantType.REPOSITORY; teamPermission.permissionType = PermissionType.TEAM; teamPermission.source = team.name; teamPermission.mutable = false; set.add(teamPermission); } } return new ArrayList(set); } /** * Returns true if the user has any type of specified access permission for * this repository. * * @param name * @return true if user has a specified access permission for the repository */ public boolean hasRepositoryPermission(String name) { String repository = AccessPermission.repositoryFromRole(name).toLowerCase(); if (permissions.containsKey(repository)) { // exact repository permission specified return true; } else { // search for regex permission match for (String key : permissions.keySet()) { if (name.matches(key)) { AccessPermission p = permissions.get(key); if (p != null) { return true; } } } } return false; } /** * Returns true if the user has an explicitly specified access permission for * this repository. * * @param name * @return if the user has an explicitly specified access permission */ public boolean hasExplicitRepositoryPermission(String name) { String repository = AccessPermission.repositoryFromRole(name).toLowerCase(); return permissions.containsKey(repository); } /** * Returns true if the user's team memberships specify an access permission for * this repository. * * @param name * @return if the user's team memberships specifi an access permission */ public boolean hasTeamRepositoryPermission(String name) { if (teams != null) { for (TeamModel team : teams) { if (team.hasRepositoryPermission(name)) { return true; } } } return false; } /** * Adds a repository permission to the team. *

* Role may be formatted as: *

    *
  • myrepo.git (this is implicitly RW+) *
  • RW+:myrepo.git *
* @param role */ public void addRepositoryPermission(String role) { AccessPermission permission = AccessPermission.permissionFromRole(role); String repository = AccessPermission.repositoryFromRole(role).toLowerCase(); repositories.add(repository); permissions.put(repository, permission); } public AccessPermission removeRepositoryPermission(String name) { String repository = AccessPermission.repositoryFromRole(name).toLowerCase(); repositories.remove(repository); return permissions.remove(repository); } public void setRepositoryPermission(String repository, AccessPermission permission) { if (permission == null) { // remove the permission permissions.remove(repository.toLowerCase()); } else { // set the new permission permissions.put(repository.toLowerCase(), permission); } } public RegistrantAccessPermission getRepositoryPermission(RepositoryModel repository) { RegistrantAccessPermission ap = new RegistrantAccessPermission(); ap.registrant = username; ap.registrantType = RegistrantType.USER; ap.permission = AccessPermission.NONE; ap.mutable = false; // determine maximum permission for the repository final AccessPermission maxPermission = (repository.isFrozen || !repository.isBare || repository.isMirror) ? AccessPermission.CLONE : AccessPermission.REWIND; if (AccessRestrictionType.NONE.equals(repository.accessRestriction)) { // anonymous rewind ap.permissionType = PermissionType.ANONYMOUS; if (AccessPermission.REWIND.atMost(maxPermission)) { ap.permission = AccessPermission.REWIND; } else { ap.permission = maxPermission; } return ap; } // administrator if (canAdmin()) { ap.permissionType = PermissionType.ADMINISTRATOR; if (AccessPermission.REWIND.atMost(maxPermission)) { ap.permission = AccessPermission.REWIND; } else { ap.permission = maxPermission; } if (!canAdmin) { // administator permission from team membership for (TeamModel team : teams) { if (team.canAdmin) { ap.source = team.name; break; } } } return ap; } // repository owner - either specified owner or personal repository if (repository.isOwner(username) || repository.isUsersPersonalRepository(username)) { ap.permissionType = PermissionType.OWNER; if (AccessPermission.REWIND.atMost(maxPermission)) { ap.permission = AccessPermission.REWIND; } else { ap.permission = maxPermission; } return ap; } if (AuthorizationControl.AUTHENTICATED.equals(repository.authorizationControl) && isAuthenticated) { // AUTHENTICATED is a shortcut for authorizing all logged-in users RW+ access if (AccessPermission.REWIND.atMost(maxPermission)) { ap.permission = AccessPermission.REWIND; } else { ap.permission = maxPermission; } return ap; } // explicit user permission OR user regex match is used // if that fails, then the best team permission is used if (permissions.containsKey(repository.name.toLowerCase())) { // exact repository permission specified, use it AccessPermission p = permissions.get(repository.name.toLowerCase()); if (p != null && repository.accessRestriction.isValidPermission(p)) { ap.permissionType = PermissionType.EXPLICIT; if (p.atMost(maxPermission)) { ap.permission = p; } else { ap.permission = maxPermission; } ap.mutable = true; return ap; } } else { // search for case-insensitive regex permission match for (String key : permissions.keySet()) { if (StringUtils.matchesIgnoreCase(repository.name, key)) { AccessPermission p = permissions.get(key); if (p != null && repository.accessRestriction.isValidPermission(p)) { // take first match ap.permissionType = PermissionType.REGEX; if (p.atMost(maxPermission)) { ap.permission = p; } else { ap.permission = maxPermission; } ap.source = key; return ap; } } } } // try to find a team match for (TeamModel team : teams) { RegistrantAccessPermission p = team.getRepositoryPermission(repository); if (p.permission.atMost(maxPermission) && p.permission.exceeds(ap.permission) && PermissionType.ANONYMOUS != p.permissionType) { // use highest team permission that is not an implicit permission ap.permission = p.permission; ap.source = team.name; ap.permissionType = PermissionType.TEAM; } } // still no explicit, regex, or team match, check for implicit permissions if (AccessPermission.NONE == ap.permission) { switch (repository.accessRestriction) { case VIEW: // no implicit permissions possible break; case CLONE: // implied view permission ap.permission = AccessPermission.VIEW; ap.permissionType = PermissionType.ANONYMOUS; break; case PUSH: // implied clone permission ap.permission = AccessPermission.CLONE; ap.permissionType = PermissionType.ANONYMOUS; break; case NONE: // implied REWIND or CLONE ap.permission = maxPermission; ap.permissionType = PermissionType.ANONYMOUS; break; } } return ap; } protected boolean canAccess(RepositoryModel repository, AccessRestrictionType ifRestriction, AccessPermission requirePermission) { if (repository.accessRestriction.atLeast(ifRestriction)) { RegistrantAccessPermission ap = getRepositoryPermission(repository); return ap.permission.atLeast(requirePermission); } return true; } public boolean canView(RepositoryModel repository) { return canAccess(repository, AccessRestrictionType.VIEW, AccessPermission.VIEW); } public boolean canView(RepositoryModel repository, String ref) { // Default UserModel doesn't implement ref-level security. // Other Realms (i.e. Gerrit) may override this method. return canView(repository); } public boolean canClone(RepositoryModel repository) { return canAccess(repository, AccessRestrictionType.CLONE, AccessPermission.CLONE); } public boolean canPush(RepositoryModel repository) { if (repository.isFrozen) { return false; } return canAccess(repository, AccessRestrictionType.PUSH, AccessPermission.PUSH); } public boolean canCreateRef(RepositoryModel repository) { if (repository.isFrozen) { return false; } return canAccess(repository, AccessRestrictionType.PUSH, AccessPermission.CREATE); } public boolean canDeleteRef(RepositoryModel repository) { if (repository.isFrozen) { return false; } return canAccess(repository, AccessRestrictionType.PUSH, AccessPermission.DELETE); } public boolean canRewindRef(RepositoryModel repository) { if (repository.isFrozen) { return false; } return canAccess(repository, AccessRestrictionType.PUSH, AccessPermission.REWIND); } public boolean canFork(RepositoryModel repository) { if (repository.isUsersPersonalRepository(username)) { // can not fork your own repository return false; } if (canAdmin() || repository.isOwner(username)) { return true; } if (!repository.allowForks) { return false; } if (!isAuthenticated || !canFork()) { return false; } return canClone(repository); } public boolean canDelete(RepositoryModel model) { return canAdmin() || model.isUsersPersonalRepository(username); } public boolean canEdit(RepositoryModel model) { return canAdmin() || model.isUsersPersonalRepository(username) || model.isOwner(username); } /** * This returns true if the user has fork privileges or the user has fork * privileges because of a team membership. * * @return true if the user can fork */ public boolean canFork() { if (canFork) { return true; } if (!ArrayUtils.isEmpty(teams)) { for (TeamModel team : teams) { if (team.canFork) { return true; } } } return false; } /** * This returns true if the user has admin privileges or the user has admin * privileges because of a team membership. * * @return true if the user can admin */ public boolean canAdmin() { if (canAdmin) { return true; } if (!ArrayUtils.isEmpty(teams)) { for (TeamModel team : teams) { if (team.canAdmin) { return true; } } } return false; } /** * This returns true if the user has create privileges or the user has create * privileges because of a team membership. * * @return true if the user can admin */ public boolean canCreate() { if (canCreate) { return true; } if (!ArrayUtils.isEmpty(teams)) { for (TeamModel team : teams) { if (team.canCreate) { return true; } } } return false; } /** * Returns true if the user is allowed to create the specified repository. * * @param repository * @return true if the user can create the repository */ public boolean canCreate(String repository) { if (canAdmin()) { // admins can create any repository return true; } if (canCreate) { String projectPath = StringUtils.getFirstPathElement(repository); if (!StringUtils.isEmpty(projectPath) && projectPath.equalsIgnoreCase(getPersonalPath())) { // personal repository return true; } } return false; } public boolean isTeamMember(String teamname) { for (TeamModel team : teams) { if (team.name.equalsIgnoreCase(teamname)) { return true; } } return false; } public TeamModel getTeam(String teamname) { if (teams == null) { return null; } for (TeamModel team : teams) { if (team.name.equalsIgnoreCase(teamname)) { return team; } } return null; } @Override public String getName() { return username; } public String getDisplayName() { if (StringUtils.isEmpty(displayName)) { return username; } return displayName; } public String getPersonalPath() { return ModelUtils.getPersonalPath(username); } public UserPreferences getPreferences() { return userPreferences; } @Override public int hashCode() { return username.hashCode(); } @Override public boolean equals(Object o) { if (o instanceof UserModel) { return username.equals(((UserModel) o).username); } return false; } @Override public String toString() { return username; } @Override public int compareTo(UserModel o) { return username.compareTo(o.username); } /** * Returns true if the name/email pair match this user account. * * @param name * @param email * @return true, if the name and email address match this account */ public boolean is(String name, String email) { // at a minimum a username or display name AND email address must be supplied if (StringUtils.isEmpty(name) || StringUtils.isEmpty(email)) { return false; } boolean nameVerified = name.equalsIgnoreCase(username) || name.equalsIgnoreCase(getDisplayName()); boolean emailVerified = false; if (StringUtils.isEmpty(emailAddress)) { // user account has not specified an email address // fail emailVerified = false; } else { // user account has specified an email address emailVerified = email.equalsIgnoreCase(emailAddress); } return nameVerified && emailVerified; } public boolean isMyPersonalRepository(String repository) { String projectPath = StringUtils.getFirstPathElement(repository); return !StringUtils.isEmpty(projectPath) && projectPath.equalsIgnoreCase(getPersonalPath()); } }