| // 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.CallerFinder; | |
| 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 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). */ | |
| class RefControl { | |
| 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; | |
| private final CallerFinder callerFinder; | |
| // The next 4 members are cached canPerform() permissions. | |
| private Boolean owner; | |
| private Boolean canForgeAuthor; | |
| private Boolean canForgeCommitter; | |
| private Boolean hasReadPermissionOnRef; | |
| RefControl( | |
| ChangeData.Factory changeDataFactory, | |
| RefVisibilityControl refVisibilityControl, | |
| ProjectControl projectControl, | |
| GitRepositoryManager repositoryManager, | |
| String ref, | |
| PermissionCollection relevant) { | |
| this.changeDataFactory = changeDataFactory; | |
| this.refVisibilityControl = refVisibilityControl; | |
| this.projectControl = projectControl; | |
| this.repositoryManager = repositoryManager; | |
| this.refName = ref; | |
| this.relevant = relevant; | |
| this.callerFinder = | |
| CallerFinder.builder() | |
| .addTarget(PermissionBackend.class) | |
| .matchSubClasses(true) | |
| .matchInnerClasses(true) | |
| .skip(1) | |
| .build(); | |
| } | |
| ProjectControl getProjectControl() { | |
| return projectControl; | |
| } | |
| CurrentUser getUser() { | |
| return projectControl.getUser(); | |
| } | |
| /** 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 */ | |
| 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() { | |
| return canPerform(Permission.EDIT_TOPIC_NAME, false, 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); | |
| } | |
| private PermissionRange toRange(String permissionName, boolean isChangeOwner) { | |
| int blockAllowMin = Integer.MIN_VALUE, blockAllowMax = Integer.MAX_VALUE; | |
| projectLoop: | |
| for (List<Permission> 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<List<Permission>> downwardPerProject = relevant.getBlockRules(permissionName); | |
| projectLoop: | |
| for (List<Permission> 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); | |
| logger.atFine().log("%s (caller: %s)", logMessage, callerFinder.findCallerLazy()); | |
| } | |
| 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 (caller: %s)", logMessage, callerFinder.findCallerLazy()); | |
| } | |
| 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 (caller: %s)", logMessage, callerFinder.findCallerLazy()); | |
| } | |
| 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<RefPermission> test(Collection<RefPermission> permSet) | |
| throws PermissionBackendException { | |
| EnumSet<RefPermission> 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()); | |
| } | |
| private 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)); | |
| } | |
| } |