LCOV - code coverage report
Current view: top level - server/permissions - RefControl.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 256 283 90.5 %
Date: 2022-11-19 15:00:39 Functions: 41 42 97.6 %

          Line data    Source code
       1             : // Copyright (C) 2010 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.permissions;
      16             : 
      17             : import static com.google.common.base.Preconditions.checkArgument;
      18             : 
      19             : import com.google.common.collect.ImmutableList;
      20             : import com.google.common.flogger.FluentLogger;
      21             : import com.google.gerrit.common.Nullable;
      22             : import com.google.gerrit.entities.Change;
      23             : import com.google.gerrit.entities.Permission;
      24             : import com.google.gerrit.entities.PermissionRange;
      25             : import com.google.gerrit.entities.PermissionRule;
      26             : import com.google.gerrit.entities.PermissionRule.Action;
      27             : import com.google.gerrit.entities.Project;
      28             : import com.google.gerrit.entities.RefNames;
      29             : import com.google.gerrit.exceptions.StorageException;
      30             : import com.google.gerrit.extensions.conditions.BooleanCondition;
      31             : import com.google.gerrit.extensions.restapi.AuthException;
      32             : import com.google.gerrit.server.CurrentUser;
      33             : import com.google.gerrit.server.git.GitRepositoryManager;
      34             : import com.google.gerrit.server.logging.CallerFinder;
      35             : import com.google.gerrit.server.logging.LoggingContext;
      36             : import com.google.gerrit.server.notedb.ChangeNotes;
      37             : import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
      38             : import com.google.gerrit.server.permissions.PermissionBackend.ForRef;
      39             : import com.google.gerrit.server.query.change.ChangeData;
      40             : import com.google.gerrit.server.util.MagicBranch;
      41             : import java.io.IOException;
      42             : import java.util.Collection;
      43             : import java.util.EnumSet;
      44             : import java.util.List;
      45             : import java.util.Set;
      46             : import java.util.concurrent.TimeUnit;
      47             : import org.eclipse.jgit.lib.Constants;
      48             : import org.eclipse.jgit.lib.Ref;
      49             : import org.eclipse.jgit.lib.Repository;
      50             : 
      51             : /** Manages access control for Git references (aka branches, tags). */
      52             : class RefControl {
      53         145 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      54             : 
      55             :   private final ChangeData.Factory changeDataFactory;
      56             :   private final RefVisibilityControl refVisibilityControl;
      57             :   private final ProjectControl projectControl;
      58             :   private final GitRepositoryManager repositoryManager;
      59             :   private final String refName;
      60             : 
      61             :   /** All permissions that apply to this reference. */
      62             :   private final PermissionCollection relevant;
      63             : 
      64             :   private final CallerFinder callerFinder;
      65             : 
      66             :   // The next 4 members are cached canPerform() permissions.
      67             : 
      68             :   private Boolean owner;
      69             :   private Boolean canForgeAuthor;
      70             :   private Boolean canForgeCommitter;
      71             :   private Boolean hasReadPermissionOnRef;
      72             : 
      73             :   RefControl(
      74             :       ChangeData.Factory changeDataFactory,
      75             :       RefVisibilityControl refVisibilityControl,
      76             :       ProjectControl projectControl,
      77             :       GitRepositoryManager repositoryManager,
      78             :       String ref,
      79         145 :       PermissionCollection relevant) {
      80         145 :     this.changeDataFactory = changeDataFactory;
      81         145 :     this.refVisibilityControl = refVisibilityControl;
      82         145 :     this.projectControl = projectControl;
      83         145 :     this.repositoryManager = repositoryManager;
      84         145 :     this.refName = ref;
      85         145 :     this.relevant = relevant;
      86         145 :     this.callerFinder =
      87         145 :         CallerFinder.builder()
      88         145 :             .addTarget(PermissionBackend.class)
      89         145 :             .matchSubClasses(true)
      90         145 :             .matchInnerClasses(true)
      91         145 :             .skip(1)
      92         145 :             .build();
      93         145 :   }
      94             : 
      95             :   ProjectControl getProjectControl() {
      96         103 :     return projectControl;
      97             :   }
      98             : 
      99             :   CurrentUser getUser() {
     100         132 :     return projectControl.getUser();
     101             :   }
     102             : 
     103             :   /** Is this user a ref owner? */
     104             :   boolean isOwner() {
     105          35 :     if (owner == null) {
     106          35 :       if (canPerform(Permission.OWNER)) {
     107           3 :         owner = true;
     108             : 
     109             :       } else {
     110          35 :         owner = projectControl.isOwner();
     111             :       }
     112             :     }
     113          35 :     return owner;
     114             :   }
     115             : 
     116             :   /**
     117             :    * Returns {@code true} if the user has permission to read the ref. This method evaluates {@link
     118             :    * RefPermission#READ} only. Hence, it is not authoritative. For example, it does not tell if the
     119             :    * user can see NoteDb refs such as {@code refs/meta/external-ids} which requires {@link
     120             :    * GlobalPermission#ACCESS_DATABASE} and deny access in this case.
     121             :    */
     122             :   boolean hasReadPermissionOnRef(boolean allowNoteDbRefs) {
     123             :     // Don't allow checking for NoteDb refs unless instructed otherwise.
     124         127 :     if (!allowNoteDbRefs
     125         126 :         && (refName.startsWith(Constants.R_TAGS) || RefNames.isGerritRef(refName))) {
     126           0 :       logger.atWarning().atMostEvery(30, TimeUnit.SECONDS).log(
     127             :           "%s: Can't determine visibility of %s in RefControl. Denying access. "
     128             :               + "This case should have been handled before.",
     129           0 :           projectControl.getProject().getName(), refName);
     130           0 :       return false;
     131             :     }
     132             : 
     133         127 :     if (hasReadPermissionOnRef == null) {
     134         127 :       hasReadPermissionOnRef = getUser().isInternalUser() || canPerform(Permission.READ);
     135             :     }
     136         127 :     return hasReadPermissionOnRef;
     137             :   }
     138             : 
     139             :   /** Returns true if this user can add a new patch set to this ref */
     140             :   boolean canAddPatchSet() {
     141          22 :     return projectControl
     142          22 :         .controlForRef(MagicBranch.NEW_CHANGE + refName)
     143          22 :         .canPerform(Permission.ADD_PATCH_SET);
     144             :   }
     145             : 
     146             :   /** Returns true if this user can rebase changes on this ref */
     147             :   boolean canRebase() {
     148          14 :     return canPerform(Permission.REBASE);
     149             :   }
     150             : 
     151             :   /** Returns true if this user can submit patch sets to this ref */
     152             :   boolean canSubmit(boolean isChangeOwner) {
     153          59 :     if (RefNames.REFS_CONFIG.equals(refName)) {
     154             :       // Always allow project owners to submit configuration changes.
     155             :       // Submitting configuration changes modifies the access control
     156             :       // rules. Allowing this to be done by a non-project-owner opens
     157             :       // a security hole enabling editing of access rules, and thus
     158             :       // granting of powers beyond submitting to the configuration.
     159           5 :       return projectControl.isOwner();
     160             :     }
     161          59 :     return canPerform(Permission.SUBMIT, isChangeOwner, false);
     162             :   }
     163             : 
     164             :   /** Returns true if this user can force edit topic names. */
     165             :   boolean canForceEditTopicName() {
     166          26 :     return canPerform(Permission.EDIT_TOPIC_NAME, false, true);
     167             :   }
     168             : 
     169             :   /** Returns true if this user can delete changes. */
     170             :   boolean canDeleteChanges(boolean isChangeOwner) {
     171          22 :     return canPerform(Permission.DELETE_CHANGES)
     172          22 :         || (isChangeOwner && canPerform(Permission.DELETE_OWN_CHANGES, isChangeOwner, false));
     173             :   }
     174             : 
     175             :   /** The range of permitted values associated with a label permission. */
     176             :   PermissionRange getRange(String permission) {
     177           1 :     return getRange(permission, false);
     178             :   }
     179             : 
     180             :   /** The range of permitted values associated with a label permission. */
     181             :   @Nullable
     182             :   PermissionRange getRange(String permission, boolean isChangeOwner) {
     183         103 :     if (Permission.hasRange(permission)) {
     184         103 :       return toRange(permission, isChangeOwner);
     185             :     }
     186           0 :     return null;
     187             :   }
     188             : 
     189             :   /** True if the user has this permission. Works only for non labels. */
     190             :   boolean canPerform(String permissionName) {
     191         145 :     return canPerform(permissionName, false, false);
     192             :   }
     193             : 
     194             :   ForRef asForRef() {
     195         132 :     return new ForRefImpl();
     196             :   }
     197             : 
     198             :   private boolean canUpload() {
     199         104 :     return projectControl.controlForRef("refs/for/" + refName).canPerform(Permission.PUSH);
     200             :   }
     201             : 
     202             :   boolean canRevert() {
     203          27 :     return canPerform(Permission.REVERT);
     204             :   }
     205             : 
     206             :   /** Returns true if this user can submit merge patch sets to this ref */
     207             :   private boolean canUploadMerges() {
     208          26 :     return projectControl.controlForRef("refs/for/" + refName).canPerform(Permission.PUSH_MERGE);
     209             :   }
     210             : 
     211             :   /** Returns true if the user can update the reference as a fast-forward. */
     212             :   private boolean canUpdate() {
     213          48 :     if (RefNames.REFS_CONFIG.equals(refName) && !projectControl.isOwner()) {
     214             :       // Pushing requires being at least project owner, in addition to push.
     215             :       // Pushing configuration changes modifies the access control
     216             :       // rules. Allowing this to be done by a non-project-owner opens
     217             :       // a security hole enabling editing of access rules, and thus
     218             :       // granting of powers beyond pushing to the configuration.
     219             : 
     220             :       // On the AllProjects project the owner access right cannot be assigned,
     221             :       // this why for the AllProjects project we allow administrators to push
     222             :       // configuration changes if they have push without being project owner.
     223           1 :       if (!(projectControl.getProjectState().isAllProjects() && projectControl.isAdmin())) {
     224           1 :         return false;
     225             :       }
     226             :     }
     227          48 :     return canPerform(Permission.PUSH);
     228             :   }
     229             : 
     230             :   /** Returns true if the user can rewind (force push) the reference. */
     231             :   private boolean canForceUpdate() {
     232          13 :     if (canPushWithForce()) {
     233          12 :       return true;
     234             :     }
     235             : 
     236           8 :     switch (getUser().getAccessPath()) {
     237             :       case GIT:
     238           7 :         return false;
     239             : 
     240             :       case REST_API:
     241             :       case SSH_COMMAND:
     242             :       case UNKNOWN:
     243             :       case WEB_BROWSER:
     244             :       default:
     245           1 :         return (isOwner() && !isBlocked(Permission.PUSH, false, true)) || projectControl.isAdmin();
     246             :     }
     247             :   }
     248             : 
     249             :   private boolean canPushWithForce() {
     250          44 :     if (RefNames.REFS_CONFIG.equals(refName) && !projectControl.isOwner()) {
     251             :       // Pushing requires being at least project owner, in addition to push.
     252             :       // Pushing configuration changes modifies the access control
     253             :       // rules. Allowing this to be done by a non-project-owner opens
     254             :       // a security hole enabling editing of access rules, and thus
     255             :       // granting of powers beyond pushing to the configuration.
     256           0 :       return false;
     257             :     }
     258          44 :     return canPerform(Permission.PUSH, false, true);
     259             :   }
     260             : 
     261             :   /**
     262             :    * Determines whether the user can delete the Git ref controlled by this object.
     263             :    *
     264             :    * @return {@code true} if the user specified can delete a Git ref.
     265             :    */
     266             :   private boolean canDelete() {
     267          41 :     switch (getUser().getAccessPath()) {
     268             :       case GIT:
     269          11 :         return canPushWithForce() || canPerform(Permission.DELETE);
     270             : 
     271             :       case REST_API:
     272             :       case SSH_COMMAND:
     273             :       case UNKNOWN:
     274             :       case WEB_BROWSER:
     275             :       default:
     276          35 :         return canPushWithForce() || canPerform(Permission.DELETE) || projectControl.isAdmin();
     277             :     }
     278             :   }
     279             : 
     280             :   /** Returns true if this user can forge the author line in a commit. */
     281             :   private boolean canForgeAuthor() {
     282          36 :     if (canForgeAuthor == null) {
     283          36 :       canForgeAuthor = canPerform(Permission.FORGE_AUTHOR);
     284             :     }
     285          36 :     return canForgeAuthor;
     286             :   }
     287             : 
     288             :   /** Returns true if this user can forge the committer line in a commit. */
     289             :   private boolean canForgeCommitter() {
     290          30 :     if (canForgeCommitter == null) {
     291          30 :       canForgeCommitter = canPerform(Permission.FORGE_COMMITTER);
     292             :     }
     293          30 :     return canForgeCommitter;
     294             :   }
     295             : 
     296             :   /** Returns true if this user can forge the server on the committer line. */
     297             :   private boolean canForgeGerritServerIdentity() {
     298           5 :     return canPerform(Permission.FORGE_SERVER);
     299             :   }
     300             : 
     301             :   private static boolean isAllow(PermissionRule pr, boolean withForce) {
     302         145 :     return pr.getAction() == Action.ALLOW && (pr.getForce() || !withForce);
     303             :   }
     304             : 
     305             :   private static boolean isBlock(PermissionRule pr, boolean withForce) {
     306             :     // BLOCK with force specified is a weaker rule than without.
     307          49 :     return pr.getAction() == Action.BLOCK && (!pr.getForce() || withForce);
     308             :   }
     309             : 
     310             :   private PermissionRange toRange(String permissionName, boolean isChangeOwner) {
     311         103 :     int blockAllowMin = Integer.MIN_VALUE, blockAllowMax = Integer.MAX_VALUE;
     312             : 
     313             :     projectLoop:
     314         103 :     for (List<Permission> ps : relevant.getBlockRules(permissionName)) {
     315           3 :       boolean blockFound = false;
     316           3 :       int projectBlockAllowMin = Integer.MIN_VALUE, projectBlockAllowMax = Integer.MAX_VALUE;
     317             : 
     318           3 :       for (Permission p : ps) {
     319           3 :         if (p.getExclusiveGroup()) {
     320           1 :           for (PermissionRule pr : p.getRules()) {
     321           1 :             if (pr.getAction() == Action.ALLOW && projectControl.match(pr, isChangeOwner)) {
     322             :               // exclusive override, usually for a more specific ref.
     323           1 :               continue projectLoop;
     324             :             }
     325           0 :           }
     326             :         }
     327             : 
     328           3 :         for (PermissionRule pr : p.getRules()) {
     329           3 :           if (pr.getAction() == Action.BLOCK && projectControl.match(pr, isChangeOwner)) {
     330           3 :             projectBlockAllowMin = pr.getMin() + 1;
     331           3 :             projectBlockAllowMax = pr.getMax() - 1;
     332           3 :             blockFound = true;
     333             :           }
     334           3 :         }
     335             : 
     336           3 :         if (blockFound) {
     337           3 :           for (PermissionRule pr : p.getRules()) {
     338           3 :             if (pr.getAction() == Action.ALLOW && projectControl.match(pr, isChangeOwner)) {
     339           1 :               projectBlockAllowMin = pr.getMin();
     340           1 :               projectBlockAllowMax = pr.getMax();
     341           1 :               break;
     342             :             }
     343           3 :           }
     344           3 :           break;
     345             :         }
     346           2 :       }
     347             : 
     348           3 :       blockAllowMin = Math.max(projectBlockAllowMin, blockAllowMin);
     349           3 :       blockAllowMax = Math.min(projectBlockAllowMax, blockAllowMax);
     350           3 :     }
     351             : 
     352         103 :     int voteMin = 0, voteMax = 0;
     353         103 :     for (PermissionRule pr : relevant.getAllowRules(permissionName)) {
     354         103 :       if (pr.getAction() == PermissionRule.Action.ALLOW
     355         103 :           && projectControl.match(pr, isChangeOwner)) {
     356             :         // For votes, contrary to normal permissions, we aggregate all applicable rules.
     357         103 :         voteMin = Math.min(voteMin, pr.getMin());
     358         103 :         voteMax = Math.max(voteMax, pr.getMax());
     359             :       }
     360         103 :     }
     361             : 
     362         103 :     return new PermissionRange(
     363             :         permissionName,
     364         103 :         /* min= */ Math.max(voteMin, blockAllowMin),
     365         103 :         /* max= */ Math.min(voteMax, blockAllowMax));
     366             :   }
     367             : 
     368             :   private boolean isBlocked(String permissionName, boolean isChangeOwner, boolean withForce) {
     369             :     // Permissions are ordered by (more general project, more specific ref). Because Permission
     370             :     // does not have back pointers, we can't tell what ref-pattern or project each permission comes
     371             :     // from.
     372         145 :     List<List<Permission>> downwardPerProject = relevant.getBlockRules(permissionName);
     373             : 
     374             :     projectLoop:
     375         145 :     for (List<Permission> projectRules : downwardPerProject) {
     376          49 :       boolean overrideFound = false;
     377          49 :       for (Permission p : projectRules) {
     378             :         // If this is an exclusive ALLOW, then block rules from the same project are ignored.
     379          49 :         if (p.getExclusiveGroup()) {
     380           1 :           for (PermissionRule pr : p.getRules()) {
     381           1 :             if (isAllow(pr, withForce) && projectControl.match(pr, isChangeOwner)) {
     382           1 :               overrideFound = true;
     383           1 :               break;
     384             :             }
     385           0 :           }
     386             :         }
     387          49 :         if (overrideFound) {
     388             :           // Found an exclusive override, nothing further to do in this project.
     389           1 :           continue projectLoop;
     390             :         }
     391             : 
     392          49 :         boolean blocked = false;
     393          49 :         for (PermissionRule pr : p.getRules()) {
     394          49 :           if (!withForce && pr.getForce()) {
     395             :             // force on block rule only applies to withForce permission.
     396           0 :             continue;
     397             :           }
     398             : 
     399          49 :           if (isBlock(pr, withForce) && projectControl.match(pr, isChangeOwner)) {
     400          49 :             blocked = true;
     401          49 :             break;
     402             :           }
     403          26 :         }
     404             : 
     405          49 :         if (blocked) {
     406             :           // ALLOW in the same AccessSection (ie. in the same Permission) overrides the BLOCK.
     407          49 :           for (PermissionRule pr : p.getRules()) {
     408          49 :             if (isAllow(pr, withForce) && projectControl.match(pr, isChangeOwner)) {
     409          23 :               blocked = false;
     410          23 :               break;
     411             :             }
     412          46 :           }
     413             :         }
     414             : 
     415          49 :         if (blocked) {
     416          46 :           return true;
     417             :         }
     418          26 :       }
     419          26 :     }
     420             : 
     421         145 :     return false;
     422             :   }
     423             : 
     424             :   /** True if the user has this permission. */
     425             :   private boolean canPerform(String permissionName, boolean isChangeOwner, boolean withForce) {
     426         145 :     if (isBlocked(permissionName, isChangeOwner, withForce)) {
     427          46 :       if (logger.atFine().isEnabled() || LoggingContext.getInstance().isAclLogging()) {
     428          11 :         String logMessage =
     429          11 :             String.format(
     430             :                 "'%s' cannot perform '%s' with force=%s on project '%s' for ref '%s'"
     431             :                     + " because this permission is blocked",
     432          11 :                 getUser().getLoggableName(),
     433             :                 permissionName,
     434          11 :                 withForce,
     435          11 :                 projectControl.getProject().getName(),
     436             :                 refName);
     437          11 :         LoggingContext.getInstance().addAclLogRecord(logMessage);
     438          11 :         logger.atFine().log("%s (caller: %s)", logMessage, callerFinder.findCallerLazy());
     439             :       }
     440          46 :       return false;
     441             :     }
     442             : 
     443         145 :     for (PermissionRule pr : relevant.getAllowRules(permissionName)) {
     444         145 :       if (isAllow(pr, withForce) && projectControl.match(pr, isChangeOwner)) {
     445         145 :         if (logger.atFine().isEnabled() || LoggingContext.getInstance().isAclLogging()) {
     446          22 :           String logMessage =
     447          22 :               String.format(
     448             :                   "'%s' can perform '%s' with force=%s on project '%s' for ref '%s'",
     449          22 :                   getUser().getLoggableName(),
     450             :                   permissionName,
     451          22 :                   withForce,
     452          22 :                   projectControl.getProject().getName(),
     453             :                   refName);
     454          22 :           LoggingContext.getInstance().addAclLogRecord(logMessage);
     455          22 :           logger.atFine().log("%s (caller: %s)", logMessage, callerFinder.findCallerLazy());
     456             :         }
     457         145 :         return true;
     458             :       }
     459          69 :     }
     460             : 
     461         117 :     if (logger.atFine().isEnabled() || LoggingContext.getInstance().isAclLogging()) {
     462          20 :       String logMessage =
     463          20 :           String.format(
     464             :               "'%s' cannot perform '%s' with force=%s on project '%s' for ref '%s'",
     465          20 :               getUser().getLoggableName(),
     466             :               permissionName,
     467          20 :               withForce,
     468          20 :               projectControl.getProject().getName(),
     469             :               refName);
     470          20 :       LoggingContext.getInstance().addAclLogRecord(logMessage);
     471          20 :       logger.atFine().log("%s (caller: %s)", logMessage, callerFinder.findCallerLazy());
     472             :     }
     473         117 :     return false;
     474             :   }
     475             : 
     476         132 :   private class ForRefImpl extends ForRef {
     477             :     private String resourcePath;
     478             : 
     479             :     @Override
     480             :     public String resourcePath() {
     481          58 :       if (resourcePath == null) {
     482          58 :         resourcePath =
     483          58 :             String.format(
     484          58 :                 "/projects/%s/+refs/%s", getProjectControl().getProjectState().getName(), refName);
     485             :       }
     486          58 :       return resourcePath;
     487             :     }
     488             : 
     489             :     @Override
     490             :     public ForChange change(ChangeData cd) {
     491             :       try {
     492         103 :         return getProjectControl().controlFor(cd).asForChange();
     493           0 :       } catch (StorageException e) {
     494           0 :         return FailedPermissionBackend.change("unavailable", e);
     495             :       }
     496             :     }
     497             : 
     498             :     @Override
     499             :     public ForChange change(ChangeNotes notes) {
     500         103 :       Project.NameKey project = getProjectControl().getProject().getNameKey();
     501         103 :       Change change = notes.getChange();
     502         103 :       checkArgument(
     503         103 :           project.equals(change.getProject()),
     504             :           "expected change in project %s, not %s",
     505             :           project,
     506         103 :           change.getProject());
     507             :       // Having ChangeNotes means it's OK to load values from NoteDb if needed.
     508             :       // ChangeData.Factory will allow lazyLoading
     509         103 :       return getProjectControl().controlFor(changeDataFactory.create(notes)).asForChange();
     510             :     }
     511             : 
     512             :     @Override
     513             :     public void check(RefPermission perm) throws AuthException, PermissionBackendException {
     514         118 :       if (!can(perm)) {
     515          25 :         PermissionDeniedException pde = new PermissionDeniedException(perm, refName);
     516          25 :         switch (perm) {
     517             :           case UPDATE:
     518           6 :             if (refName.equals(RefNames.REFS_CONFIG)) {
     519           1 :               pde.setAdvice(
     520             :                   "Configuration changes can only be pushed by project owners\n"
     521             :                       + "who also have 'Push' rights on "
     522             :                       + RefNames.REFS_CONFIG);
     523             :             } else {
     524           6 :               pde.setAdvice(
     525             :                   "Push to refs/for/"
     526           6 :                       + RefNames.shortName(refName)
     527             :                       + " to create a review, or get 'Push' rights to update the branch.");
     528             :             }
     529           6 :             break;
     530             :           case DELETE:
     531          11 :             pde.setAdvice(
     532             :                 "You need 'Delete Reference' rights or 'Push' rights with the \n"
     533             :                     + "'Force Push' flag set to delete references.");
     534          11 :             break;
     535             :           case CREATE_CHANGE:
     536             :             // This is misleading in the default permission backend, since "create change" on a
     537             :             // branch is encoded as "push" on refs/for/DESTINATION.
     538           3 :             pde.setAdvice(
     539             :                 "You need 'Create Change' rights to upload code review requests.\n"
     540             :                     + "Verify that you are pushing to the right branch.");
     541           3 :             break;
     542             :           case CREATE:
     543           8 :             pde.setAdvice("You need 'Create' rights to create new references.");
     544           8 :             break;
     545             :           case CREATE_SIGNED_TAG:
     546           0 :             pde.setAdvice("You need 'Create Signed Tag' rights to push a signed tag.");
     547           0 :             break;
     548             :           case CREATE_TAG:
     549           3 :             pde.setAdvice("You need 'Create Tag' rights to push a normal tag.");
     550           3 :             break;
     551             :           case FORCE_UPDATE:
     552           7 :             pde.setAdvice(
     553             :                 "You need 'Push' rights with 'Force' flag set to do a non-fastforward push.");
     554           7 :             break;
     555             :           case FORGE_AUTHOR:
     556           2 :             pde.setAdvice(
     557             :                 "You need 'Forge Author' rights to push commits with another user as author.");
     558           2 :             break;
     559             :           case FORGE_COMMITTER:
     560           0 :             pde.setAdvice(
     561             :                 "You need 'Forge Committer' rights to push commits with another user as"
     562             :                     + " committer.");
     563           0 :             break;
     564             :           case FORGE_SERVER:
     565           0 :             pde.setAdvice(
     566             :                 "You need 'Forge Server' rights to push merge commits authored by the server.");
     567           0 :             break;
     568             :           case MERGE:
     569           0 :             pde.setAdvice(
     570             :                 "You need 'Push Merge' in addition to 'Push' rights to push merge commits.");
     571           0 :             break;
     572             : 
     573             :           case READ:
     574           5 :             pde.setAdvice("You need 'Read' rights to fetch or clone this ref.");
     575           5 :             break;
     576             : 
     577             :           case READ_CONFIG:
     578           0 :             pde.setAdvice("You need 'Read' rights on refs/meta/config to see the configuration.");
     579           0 :             break;
     580             :           case READ_PRIVATE_CHANGES:
     581           1 :             pde.setAdvice("You need 'Read Private Changes' to see private changes.");
     582           1 :             break;
     583             :           case SET_HEAD:
     584           1 :             pde.setAdvice("You need 'Set HEAD' rights to set the default branch.");
     585           1 :             break;
     586             :           case SKIP_VALIDATION:
     587           4 :             pde.setAdvice(
     588             :                 "You need 'Forge Author', 'Forge Server', 'Forge Committer'\n"
     589             :                     + "and 'Push Merge' rights to skip validation.");
     590           4 :             break;
     591             :           case UPDATE_BY_SUBMIT:
     592           3 :             pde.setAdvice(
     593             :                 "You need 'Submit' rights on refs/for/ to submit changes during change upload.");
     594           3 :             break;
     595             : 
     596             :           case WRITE_CONFIG:
     597           1 :             pde.setAdvice("You need 'Write' rights on refs/meta/config.");
     598             :             break;
     599             :         }
     600          25 :         throw pde;
     601             :       }
     602         118 :     }
     603             : 
     604             :     @Override
     605             :     public Set<RefPermission> test(Collection<RefPermission> permSet)
     606             :         throws PermissionBackendException {
     607         129 :       EnumSet<RefPermission> ok = EnumSet.noneOf(RefPermission.class);
     608         129 :       for (RefPermission perm : permSet) {
     609         129 :         if (can(perm)) {
     610         126 :           ok.add(perm);
     611             :         }
     612         129 :       }
     613         129 :       return ok;
     614             :     }
     615             : 
     616             :     @Override
     617             :     public BooleanCondition testCond(RefPermission perm) {
     618          58 :       return new PermissionBackendCondition.ForRef(this, perm, getUser());
     619             :     }
     620             : 
     621             :     private boolean can(RefPermission perm) throws PermissionBackendException {
     622         132 :       switch (perm) {
     623             :         case READ:
     624             :           /* Internal users such as plugin users should be able to read all refs. */
     625         131 :           if (getUser().isInternalUser()) {
     626           5 :             return true;
     627             :           }
     628         131 :           if (refName.startsWith(Constants.R_TAGS)) {
     629          12 :             return isTagVisible();
     630             :           }
     631         130 :           return refVisibilityControl.isVisible(projectControl, refName);
     632             :         case CREATE:
     633             :           // TODO This isn't an accurate test.
     634          43 :           return canPerform(refPermissionName(perm));
     635             :         case DELETE:
     636          41 :           return canDelete();
     637             :         case UPDATE:
     638          48 :           return canUpdate();
     639             :         case FORCE_UPDATE:
     640          13 :           return canForceUpdate();
     641             :         case SET_HEAD:
     642           1 :           return projectControl.isOwner();
     643             : 
     644             :         case FORGE_AUTHOR:
     645          36 :           return canForgeAuthor();
     646             :         case FORGE_COMMITTER:
     647          30 :           return canForgeCommitter();
     648             :         case FORGE_SERVER:
     649           1 :           return canForgeGerritServerIdentity();
     650             :         case MERGE:
     651          26 :           return canUploadMerges();
     652             : 
     653             :         case CREATE_CHANGE:
     654         104 :           return canUpload();
     655             : 
     656             :         case CREATE_TAG:
     657             :         case CREATE_SIGNED_TAG:
     658           4 :           return canPerform(refPermissionName(perm));
     659             : 
     660             :         case UPDATE_BY_SUBMIT:
     661           9 :           return projectControl.controlForRef(MagicBranch.NEW_CHANGE + refName).canSubmit(true);
     662             : 
     663             :         case READ_PRIVATE_CHANGES:
     664          12 :           return canPerform(Permission.VIEW_PRIVATE_CHANGES);
     665             : 
     666             :         case READ_CONFIG:
     667           0 :           return projectControl
     668           0 :               .controlForRef(RefNames.REFS_CONFIG)
     669           0 :               .canPerform(RefPermission.READ.name());
     670             :         case WRITE_CONFIG:
     671          29 :           return isOwner();
     672             : 
     673             :         case SKIP_VALIDATION:
     674           4 :           return canForgeAuthor()
     675           4 :               && canForgeCommitter()
     676           4 :               && canForgeGerritServerIdentity()
     677           4 :               && canUploadMerges();
     678             :       }
     679           0 :       throw new PermissionBackendException(perm + " unsupported");
     680             :     }
     681             : 
     682             :     private boolean isTagVisible() throws PermissionBackendException {
     683          12 :       if (projectControl.asForProject().test(ProjectPermission.READ)) {
     684             :         // The user has READ on refs/* with no effective block permission. This is the broadest
     685             :         // permission one can assign. There is no way to grant access to (specific) tags in Gerrit,
     686             :         // so we have to assume that these users can see all tags because there could be tags that
     687             :         // aren't reachable by any visible ref while the user can see all non-Gerrit refs. This
     688             :         // matches Gerrit's historic behavior.
     689             :         // This makes it so that these users could see commits that they can't see otherwise
     690             :         // (e.g. a private change ref) if a tag was attached to it. Tags are meant to be used on
     691             :         // the regular Git tree that users interact with, not on any of the Gerrit trees, so this
     692             :         // is a negligible risk.
     693          12 :         return true;
     694             :       }
     695             : 
     696           5 :       try (Repository repo =
     697           5 :           repositoryManager.openRepository(projectControl.getProject().getNameKey())) {
     698             :         // Tag visibility requires going through RefFilter because it entails loading all taggable
     699             :         // refs and filtering them all by visibility.
     700           5 :         Ref resolvedRef = repo.getRefDatabase().exactRef(refName);
     701           5 :         if (resolvedRef == null) {
     702           0 :           return false;
     703             :         }
     704           5 :         return projectControl.asForProject()
     705           5 :             .filter(
     706           5 :                 ImmutableList.of(resolvedRef), repo, PermissionBackend.RefFilterOptions.defaults())
     707           5 :             .stream()
     708           5 :             .anyMatch(r -> refName.equals(r.getName()));
     709           0 :       } catch (IOException e) {
     710           0 :         throw new PermissionBackendException(e);
     711             :       }
     712             :     }
     713             :   }
     714             : 
     715             :   private static String refPermissionName(RefPermission refPermission) {
     716             :     // Within this class, it's programmer error to call this method on a
     717             :     // RefPermission that isn't associated with a permission name.
     718          45 :     return DefaultPermissionMappings.refPermissionName(refPermission)
     719          45 :         .orElseThrow(() -> new IllegalStateException("no name for " + refPermission));
     720             :   }
     721             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750