All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.permissions.RefControl Maven / Gradle / Ivy

The newest version!
// Copyright (C) 2010 The Android Open Source Project
//
// 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.google.gerrit.server.permissions;

import static com.google.common.base.Preconditions.checkArgument;

import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.PermissionRange;
import com.google.gerrit.entities.PermissionRule;
import com.google.gerrit.entities.PermissionRule.Action;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.conditions.BooleanCondition;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.logging.LoggingContext;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
import com.google.gerrit.server.permissions.PermissionBackend.ForRef;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.util.MagicBranch;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;

/**
 * Manages access control for Git references (aka branches, tags).
 *
 * 

Do not use this class directly - instead use {@link ProjectControl} class. This class is * public only because it is extended in google-owned implementation. */ public class RefControl { public interface Factory { RefControl create(ProjectControl projectControl, String ref, PermissionCollection relevant); } private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private final ChangeData.Factory changeDataFactory; private final RefVisibilityControl refVisibilityControl; private final ProjectControl projectControl; private final GitRepositoryManager repositoryManager; private final String refName; /** All permissions that apply to this reference. */ private final PermissionCollection relevant; // The next 4 members are cached canPerform() permissions. private Boolean owner; private Boolean canForgeAuthor; private Boolean canForgeCommitter; private Boolean hasReadPermissionOnRef; @Inject protected RefControl( ChangeData.Factory changeDataFactory, RefVisibilityControl refVisibilityControl, GitRepositoryManager repositoryManager, @Assisted ProjectControl projectControl, @Assisted String ref, @Assisted PermissionCollection relevant) { this.changeDataFactory = changeDataFactory; this.refVisibilityControl = refVisibilityControl; this.repositoryManager = repositoryManager; this.projectControl = projectControl; this.refName = ref; this.relevant = relevant; } protected ProjectControl getProjectControl() { return projectControl; } CurrentUser getUser() { return projectControl.getUser(); } protected String getRefName() { return refName; } /** Is this user a ref owner? */ boolean isOwner() { if (owner == null) { if (canPerform(Permission.OWNER)) { owner = true; } else { owner = projectControl.isOwner(); } } return owner; } /** * Returns {@code true} if the user has permission to read the ref. This method evaluates {@link * RefPermission#READ} only. Hence, it is not authoritative. For example, it does not tell if the * user can see NoteDb refs such as {@code refs/meta/external-ids} which requires {@link * GlobalPermission#ACCESS_DATABASE} and deny access in this case. */ boolean hasReadPermissionOnRef(boolean allowNoteDbRefs) { // Don't allow checking for NoteDb refs unless instructed otherwise. if (!allowNoteDbRefs && (refName.startsWith(Constants.R_TAGS) || RefNames.isGerritRef(refName))) { logger.atWarning().atMostEvery(30, TimeUnit.SECONDS).log( "%s: Can't determine visibility of %s in RefControl. Denying access. " + "This case should have been handled before.", projectControl.getProject().getName(), refName); return false; } if (hasReadPermissionOnRef == null) { hasReadPermissionOnRef = getUser().isInternalUser() || canPerform(Permission.READ); } return hasReadPermissionOnRef; } /** Returns true if this user can add a new patch set to this ref */ boolean canAddPatchSet() { return projectControl .controlForRef(MagicBranch.NEW_CHANGE + refName) .canPerform(Permission.ADD_PATCH_SET); } /** Returns true if this user can rebase changes on this ref */ boolean canRebase() { return canPerform(Permission.REBASE); } /** Returns true if this user can submit patch sets to this ref */ protected boolean canSubmit(boolean isChangeOwner) { if (RefNames.REFS_CONFIG.equals(refName)) { // Always allow project owners to submit configuration changes. // Submitting configuration changes modifies the access control // rules. Allowing this to be done by a non-project-owner opens // a security hole enabling editing of access rules, and thus // granting of powers beyond submitting to the configuration. return projectControl.isOwner(); } return canPerform(Permission.SUBMIT, isChangeOwner, false); } /** Returns true if this user can force edit topic names. */ boolean canForceEditTopicName(boolean isChangeOwner) { return canPerform(Permission.EDIT_TOPIC_NAME, isChangeOwner, true); } /** Returns true if this user can delete changes. */ boolean canDeleteChanges(boolean isChangeOwner) { return canPerform(Permission.DELETE_CHANGES) || (isChangeOwner && canPerform(Permission.DELETE_OWN_CHANGES, isChangeOwner, false)); } /** The range of permitted values associated with a label permission. */ PermissionRange getRange(String permission) { return getRange(permission, false); } /** The range of permitted values associated with a label permission. */ @Nullable PermissionRange getRange(String permission, boolean isChangeOwner) { if (Permission.hasRange(permission)) { return toRange(permission, isChangeOwner); } return null; } /** True if the user has this permission. Works only for non labels. */ boolean canPerform(String permissionName) { return canPerform(permissionName, false, false); } ForRef asForRef() { return new ForRefImpl(); } private boolean canUpload() { return projectControl.controlForRef("refs/for/" + refName).canPerform(Permission.PUSH); } boolean canRevert() { return canPerform(Permission.REVERT); } /** Returns true if this user can submit merge patch sets to this ref */ private boolean canUploadMerges() { return projectControl.controlForRef("refs/for/" + refName).canPerform(Permission.PUSH_MERGE); } /** Returns true if the user can update the reference as a fast-forward. */ private boolean canUpdate() { if (RefNames.REFS_CONFIG.equals(refName) && !projectControl.isOwner()) { // Pushing requires being at least project owner, in addition to push. // Pushing configuration changes modifies the access control // rules. Allowing this to be done by a non-project-owner opens // a security hole enabling editing of access rules, and thus // granting of powers beyond pushing to the configuration. // On the AllProjects project the owner access right cannot be assigned, // this why for the AllProjects project we allow administrators to push // configuration changes if they have push without being project owner. if (!(projectControl.getProjectState().isAllProjects() && projectControl.isAdmin())) { return false; } } return canPerform(Permission.PUSH); } /** Returns true if the user can rewind (force push) the reference. */ private boolean canForceUpdate() { if (canPushWithForce()) { return true; } switch (getUser().getAccessPath()) { case GIT: return false; case REST_API: case SSH_COMMAND: case UNKNOWN: case WEB_BROWSER: default: return (isOwner() && !isBlocked(Permission.PUSH, false, true)) || projectControl.isAdmin(); } } private boolean canPushWithForce() { if (RefNames.REFS_CONFIG.equals(refName) && !projectControl.isOwner()) { // Pushing requires being at least project owner, in addition to push. // Pushing configuration changes modifies the access control // rules. Allowing this to be done by a non-project-owner opens // a security hole enabling editing of access rules, and thus // granting of powers beyond pushing to the configuration. return false; } return canPerform(Permission.PUSH, false, true); } /** * Determines whether the user can delete the Git ref controlled by this object. * * @return {@code true} if the user specified can delete a Git ref. */ private boolean canDelete() { switch (getUser().getAccessPath()) { case GIT: return canPushWithForce() || canPerform(Permission.DELETE); case REST_API: case SSH_COMMAND: case UNKNOWN: case WEB_BROWSER: default: return canPushWithForce() || canPerform(Permission.DELETE) || projectControl.isAdmin(); } } /** Returns true if this user can forge the author line in a commit. */ private boolean canForgeAuthor() { if (canForgeAuthor == null) { canForgeAuthor = canPerform(Permission.FORGE_AUTHOR); } return canForgeAuthor; } /** Returns true if this user can forge the committer line in a commit. */ private boolean canForgeCommitter() { if (canForgeCommitter == null) { canForgeCommitter = canPerform(Permission.FORGE_COMMITTER); } return canForgeCommitter; } /** Returns true if this user can forge the server on the committer line. */ private boolean canForgeGerritServerIdentity() { return canPerform(Permission.FORGE_SERVER); } private static boolean isAllow(PermissionRule pr, boolean withForce) { return pr.getAction() == Action.ALLOW && (pr.getForce() || !withForce); } private static boolean isBlock(PermissionRule pr, boolean withForce) { // BLOCK with force specified is a weaker rule than without. return pr.getAction() == Action.BLOCK && (!pr.getForce() || withForce); } protected PermissionRange toRange(String permissionName, boolean isChangeOwner) { int blockAllowMin = Integer.MIN_VALUE, blockAllowMax = Integer.MAX_VALUE; projectLoop: for (List ps : relevant.getBlockRules(permissionName)) { boolean blockFound = false; int projectBlockAllowMin = Integer.MIN_VALUE, projectBlockAllowMax = Integer.MAX_VALUE; for (Permission p : ps) { if (p.getExclusiveGroup()) { for (PermissionRule pr : p.getRules()) { if (pr.getAction() == Action.ALLOW && projectControl.match(pr, isChangeOwner)) { // exclusive override, usually for a more specific ref. continue projectLoop; } } } for (PermissionRule pr : p.getRules()) { if (pr.getAction() == Action.BLOCK && projectControl.match(pr, isChangeOwner)) { projectBlockAllowMin = pr.getMin() + 1; projectBlockAllowMax = pr.getMax() - 1; blockFound = true; } } if (blockFound) { for (PermissionRule pr : p.getRules()) { if (pr.getAction() == Action.ALLOW && projectControl.match(pr, isChangeOwner)) { projectBlockAllowMin = pr.getMin(); projectBlockAllowMax = pr.getMax(); break; } } break; } } blockAllowMin = Math.max(projectBlockAllowMin, blockAllowMin); blockAllowMax = Math.min(projectBlockAllowMax, blockAllowMax); } int voteMin = 0, voteMax = 0; for (PermissionRule pr : relevant.getAllowRules(permissionName)) { if (pr.getAction() == PermissionRule.Action.ALLOW && projectControl.match(pr, isChangeOwner)) { // For votes, contrary to normal permissions, we aggregate all applicable rules. voteMin = Math.min(voteMin, pr.getMin()); voteMax = Math.max(voteMax, pr.getMax()); } } return new PermissionRange( permissionName, /* min= */ Math.max(voteMin, blockAllowMin), /* max= */ Math.min(voteMax, blockAllowMax)); } private boolean isBlocked(String permissionName, boolean isChangeOwner, boolean withForce) { // Permissions are ordered by (more general project, more specific ref). Because Permission // does not have back pointers, we can't tell what ref-pattern or project each permission comes // from. List> downwardPerProject = relevant.getBlockRules(permissionName); projectLoop: for (List projectRules : downwardPerProject) { boolean overrideFound = false; for (Permission p : projectRules) { // If this is an exclusive ALLOW, then block rules from the same project are ignored. if (p.getExclusiveGroup()) { for (PermissionRule pr : p.getRules()) { if (isAllow(pr, withForce) && projectControl.match(pr, isChangeOwner)) { overrideFound = true; break; } } } if (overrideFound) { // Found an exclusive override, nothing further to do in this project. continue projectLoop; } boolean blocked = false; for (PermissionRule pr : p.getRules()) { if (!withForce && pr.getForce()) { // force on block rule only applies to withForce permission. continue; } if (isBlock(pr, withForce) && projectControl.match(pr, isChangeOwner)) { blocked = true; break; } } if (blocked) { // ALLOW in the same AccessSection (ie. in the same Permission) overrides the BLOCK. for (PermissionRule pr : p.getRules()) { if (isAllow(pr, withForce) && projectControl.match(pr, isChangeOwner)) { blocked = false; break; } } } if (blocked) { return true; } } } return false; } /** True if the user has this permission. */ private boolean canPerform(String permissionName, boolean isChangeOwner, boolean withForce) { if (isBlocked(permissionName, isChangeOwner, withForce)) { if (logger.atFine().isEnabled() || LoggingContext.getInstance().isAclLogging()) { String logMessage = String.format( "'%s' cannot perform '%s' with force=%s on project '%s' for ref '%s'" + " because this permission is blocked", getUser().getLoggableName(), permissionName, withForce, projectControl.getProject().getName(), refName); LoggingContext.getInstance().addAclLogRecord(logMessage); } return false; } for (PermissionRule pr : relevant.getAllowRules(permissionName)) { if (isAllow(pr, withForce) && projectControl.match(pr, isChangeOwner)) { if (logger.atFine().isEnabled() || LoggingContext.getInstance().isAclLogging()) { String logMessage = String.format( "'%s' can perform '%s' with force=%s on project '%s' for ref '%s'" + " (allowed for group '%s' by rule '%s')", getUser().getLoggableName(), permissionName, withForce, projectControl.getProject().getName(), refName, pr.getGroup().getUUID().get(), pr); LoggingContext.getInstance().addAclLogRecord(logMessage); logger.atFine().log("%s", logMessage); } return true; } } if (logger.atFine().isEnabled() || LoggingContext.getInstance().isAclLogging()) { String logMessage = String.format( "'%s' cannot perform '%s' with force=%s on project '%s' for ref '%s'", getUser().getLoggableName(), permissionName, withForce, projectControl.getProject().getName(), refName); LoggingContext.getInstance().addAclLogRecord(logMessage); logger.atFine().log("%s", logMessage); } return false; } private class ForRefImpl extends ForRef { private String resourcePath; @Override public String resourcePath() { if (resourcePath == null) { resourcePath = String.format( "/projects/%s/+refs/%s", getProjectControl().getProjectState().getName(), refName); } return resourcePath; } @Override public ForChange change(ChangeData cd) { try { return getProjectControl().controlFor(cd).asForChange(); } catch (StorageException e) { return FailedPermissionBackend.change("unavailable", e); } } @Override public ForChange change(ChangeNotes notes) { Project.NameKey project = getProjectControl().getProject().getNameKey(); Change change = notes.getChange(); checkArgument( project.equals(change.getProject()), "expected change in project %s, not %s", project, change.getProject()); // Having ChangeNotes means it's OK to load values from NoteDb if needed. // ChangeData.Factory will allow lazyLoading return getProjectControl().controlFor(changeDataFactory.create(notes)).asForChange(); } @Override public void check(RefPermission perm) throws AuthException, PermissionBackendException { if (!can(perm)) { PermissionDeniedException pde = new PermissionDeniedException(perm, refName); switch (perm) { case UPDATE: if (refName.equals(RefNames.REFS_CONFIG)) { pde.setAdvice( "Configuration changes can only be pushed by project owners\n" + "who also have 'Push' rights on " + RefNames.REFS_CONFIG); } else { pde.setAdvice( "Push to refs/for/" + RefNames.shortName(refName) + " to create a review, or get 'Push' rights to update the branch."); } break; case DELETE: pde.setAdvice( "You need 'Delete Reference' rights or 'Push' rights with the \n" + "'Force Push' flag set to delete references."); break; case CREATE_CHANGE: // This is misleading in the default permission backend, since "create change" on a // branch is encoded as "push" on refs/for/DESTINATION. pde.setAdvice( "You need 'Create Change' rights to upload code review requests.\n" + "Verify that you are pushing to the right branch."); break; case CREATE: pde.setAdvice("You need 'Create' rights to create new references."); break; case CREATE_SIGNED_TAG: pde.setAdvice("You need 'Create Signed Tag' rights to push a signed tag."); break; case CREATE_TAG: pde.setAdvice("You need 'Create Tag' rights to push a normal tag."); break; case FORCE_UPDATE: pde.setAdvice( "You need 'Push' rights with 'Force' flag set to do a non-fastforward push."); break; case FORGE_AUTHOR: pde.setAdvice( "You need 'Forge Author' rights to push commits with another user as author."); break; case FORGE_COMMITTER: pde.setAdvice( "You need 'Forge Committer' rights to push commits with another user as" + " committer."); break; case FORGE_SERVER: pde.setAdvice( "You need 'Forge Server' rights to push merge commits authored by the server."); break; case MERGE: pde.setAdvice( "You need 'Push Merge' in addition to 'Push' rights to push merge commits."); break; case READ: pde.setAdvice("You need 'Read' rights to fetch or clone this ref."); break; case READ_CONFIG: pde.setAdvice("You need 'Read' rights on refs/meta/config to see the configuration."); break; case READ_PRIVATE_CHANGES: pde.setAdvice("You need 'Read Private Changes' to see private changes."); break; case SET_HEAD: pde.setAdvice("You need 'Set HEAD' rights to set the default branch."); break; case SKIP_VALIDATION: pde.setAdvice( "You need 'Forge Author', 'Forge Server', 'Forge Committer'\n" + "and 'Push Merge' rights to skip validation."); break; case UPDATE_BY_SUBMIT: pde.setAdvice( "You need 'Submit' rights on refs/for/ to submit changes during change upload."); break; case WRITE_CONFIG: pde.setAdvice("You need 'Write' rights on refs/meta/config."); break; } throw pde; } } @Override public Set test(Collection permSet) throws PermissionBackendException { EnumSet ok = EnumSet.noneOf(RefPermission.class); for (RefPermission perm : permSet) { if (can(perm)) { ok.add(perm); } } return ok; } @Override public BooleanCondition testCond(RefPermission perm) { return new PermissionBackendCondition.ForRef(this, perm, getUser()); } } protected boolean can(RefPermission perm) throws PermissionBackendException { switch (perm) { case READ: /* Internal users such as plugin users should be able to read all refs. */ if (getUser().isInternalUser()) { return true; } if (refName.startsWith(Constants.R_TAGS)) { return isTagVisible(); } return refVisibilityControl.isVisible(projectControl, refName); case CREATE: // TODO This isn't an accurate test. return canPerform(refPermissionName(perm)); case DELETE: return canDelete(); case UPDATE: return canUpdate(); case FORCE_UPDATE: return canForceUpdate(); case SET_HEAD: return projectControl.isOwner(); case FORGE_AUTHOR: return canForgeAuthor(); case FORGE_COMMITTER: return canForgeCommitter(); case FORGE_SERVER: return canForgeGerritServerIdentity(); case MERGE: return canUploadMerges(); case CREATE_CHANGE: return canUpload(); case CREATE_TAG: case CREATE_SIGNED_TAG: return canPerform(refPermissionName(perm)); case UPDATE_BY_SUBMIT: return projectControl.controlForRef(MagicBranch.NEW_CHANGE + refName).canSubmit(true); case READ_PRIVATE_CHANGES: return canPerform(Permission.VIEW_PRIVATE_CHANGES); case READ_CONFIG: return projectControl .controlForRef(RefNames.REFS_CONFIG) .canPerform(RefPermission.READ.name()); case WRITE_CONFIG: return isOwner(); case SKIP_VALIDATION: return canForgeAuthor() && canForgeCommitter() && canForgeGerritServerIdentity() && canUploadMerges(); } throw new PermissionBackendException(perm + " unsupported"); } private boolean isTagVisible() throws PermissionBackendException { if (projectControl.asForProject().test(ProjectPermission.READ)) { // The user has READ on refs/* with no effective block permission. This is the broadest // permission one can assign. There is no way to grant access to (specific) tags in Gerrit, // so we have to assume that these users can see all tags because there could be tags that // aren't reachable by any visible ref while the user can see all non-Gerrit refs. This // matches Gerrit's historic behavior. // This makes it so that these users could see commits that they can't see otherwise // (e.g. a private change ref) if a tag was attached to it. Tags are meant to be used on // the regular Git tree that users interact with, not on any of the Gerrit trees, so this // is a negligible risk. return true; } try (Repository repo = repositoryManager.openRepository(projectControl.getProject().getNameKey())) { // Tag visibility requires going through RefFilter because it entails loading all taggable // refs and filtering them all by visibility. Ref resolvedRef = repo.getRefDatabase().exactRef(refName); if (resolvedRef == null) { return false; } return projectControl .asForProject() .filter( ImmutableList.of(resolvedRef), repo, PermissionBackend.RefFilterOptions.defaults()) .stream() .anyMatch(r -> refName.equals(r.getName())); } catch (IOException e) { throw new PermissionBackendException(e); } } private static String refPermissionName(RefPermission refPermission) { // Within this class, it's programmer error to call this method on a // RefPermission that isn't associated with a permission name. return DefaultPermissionMappings.refPermissionName(refPermission) .orElseThrow(() -> new IllegalStateException("no name for " + refPermission)); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy