| // 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(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); |
| } |
| |
| 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'", |
| getUser().getLoggableName(), |
| permissionName, |
| withForce, |
| projectControl.getProject().getName(), |
| refName); |
| 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)); |
| } |
| } |