LCOV - code coverage report
Current view: top level - server/project - CreateRefControl.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 64 71 90.1 %
Date: 2022-11-19 15:00:39 Functions: 5 5 100.0 %

          Line data    Source code
       1             : // Copyright (C) 2017 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.project;
      16             : 
      17             : import static com.google.gerrit.server.project.ProjectCache.noSuchProject;
      18             : 
      19             : import com.google.common.flogger.FluentLogger;
      20             : import com.google.gerrit.entities.BranchNameKey;
      21             : import com.google.gerrit.entities.Project;
      22             : import com.google.gerrit.extensions.restapi.AuthException;
      23             : import com.google.gerrit.extensions.restapi.ResourceConflictException;
      24             : import com.google.gerrit.server.CurrentUser;
      25             : import com.google.gerrit.server.permissions.PermissionBackend;
      26             : import com.google.gerrit.server.permissions.PermissionBackendException;
      27             : import com.google.gerrit.server.permissions.RefPermission;
      28             : import com.google.gerrit.server.query.change.ChangeData;
      29             : import com.google.gerrit.server.update.RetryHelper;
      30             : import com.google.inject.Inject;
      31             : import com.google.inject.Provider;
      32             : import com.google.inject.Singleton;
      33             : import java.io.IOException;
      34             : import java.util.List;
      35             : import java.util.Optional;
      36             : import org.eclipse.jgit.lib.Constants;
      37             : import org.eclipse.jgit.lib.PersonIdent;
      38             : import org.eclipse.jgit.lib.Repository;
      39             : import org.eclipse.jgit.revwalk.RevCommit;
      40             : import org.eclipse.jgit.revwalk.RevObject;
      41             : import org.eclipse.jgit.revwalk.RevTag;
      42             : import org.eclipse.jgit.revwalk.RevWalk;
      43             : 
      44             : /** Manages access control for creating Git references (aka branches, tags). */
      45             : @Singleton
      46             : public class CreateRefControl {
      47             : 
      48         138 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      49             : 
      50             :   private final PermissionBackend permissionBackend;
      51             :   private final ProjectCache projectCache;
      52             :   private final Reachable reachable;
      53             :   private final RetryHelper retryHelper;
      54             : 
      55             :   @Inject
      56             :   CreateRefControl(
      57             :       PermissionBackend permissionBackend,
      58             :       ProjectCache projectCache,
      59             :       Reachable reachable,
      60         138 :       RetryHelper retryHelper) {
      61         138 :     this.permissionBackend = permissionBackend;
      62         138 :     this.projectCache = projectCache;
      63         138 :     this.reachable = reachable;
      64         138 :     this.retryHelper = retryHelper;
      65         138 :   }
      66             : 
      67             :   /**
      68             :    * Checks whether the {@link CurrentUser} can create a new Git ref.
      69             :    *
      70             :    * @param user the user performing the operation
      71             :    * @param repo repository on which user want to create
      72             :    * @param destBranch the branch the new {@link RevObject} should be created on
      73             :    * @param object the object the user will start the reference with
      74             :    * @param sourceBranches the source ref from which the new ref is created from
      75             :    * @throws AuthException if creation is denied; the message explains the denial.
      76             :    * @throws PermissionBackendException on failure of permission checks.
      77             :    * @throws ResourceConflictException if the project state does not permit the operation
      78             :    */
      79             :   public void checkCreateRef(
      80             :       Provider<? extends CurrentUser> user,
      81             :       Repository repo,
      82             :       BranchNameKey destBranch,
      83             :       RevObject object,
      84             :       boolean forPush,
      85             :       BranchNameKey... sourceBranches)
      86             :       throws AuthException, PermissionBackendException, NoSuchProjectException, IOException,
      87             :           ResourceConflictException {
      88          43 :     ProjectState ps =
      89          43 :         projectCache.get(destBranch.project()).orElseThrow(noSuchProject(destBranch.project()));
      90          43 :     ps.checkStatePermitsWrite();
      91             : 
      92          43 :     PermissionBackend.ForRef perm = permissionBackend.user(user.get()).ref(destBranch);
      93          43 :     if (object instanceof RevCommit) {
      94          41 :       perm.check(RefPermission.CREATE);
      95          41 :       if (sourceBranches.length == 0) {
      96          32 :         checkCreateCommit(user, repo, (RevCommit) object, ps.getNameKey(), perm, forPush);
      97             :       } else {
      98          27 :         for (BranchNameKey src : sourceBranches) {
      99          27 :           PermissionBackend.ForRef forRef = permissionBackend.user(user.get()).ref(src);
     100          27 :           if (forRef.testOrFalse(RefPermission.READ)) {
     101          27 :             return;
     102             :           }
     103             :         }
     104           1 :         AuthException e =
     105             :             new AuthException(
     106           1 :                 String.format(
     107             :                     "must have %s on existing ref to create new ref from it",
     108           1 :                     RefPermission.READ.describeForException()));
     109           1 :         e.setAdvice(
     110           1 :             String.format(
     111             :                 "use an existing ref visible to you, or get %s permission on the ref",
     112           1 :                 RefPermission.READ.describeForException()));
     113           1 :         throw e;
     114             :       }
     115           2 :     } else if (object instanceof RevTag) {
     116           2 :       RevTag tag = (RevTag) object;
     117           2 :       try (RevWalk rw = new RevWalk(repo)) {
     118           2 :         rw.parseBody(tag);
     119           0 :       } catch (IOException e) {
     120           0 :         logger.atSevere().withCause(e).log(
     121           0 :             "RevWalk(%s) parsing %s:", destBranch.project(), tag.name());
     122           0 :         throw e;
     123           2 :       }
     124             : 
     125             :       // If tagger is present, require it matches the user's email.
     126           2 :       PersonIdent tagger = tag.getTaggerIdent();
     127           2 :       if (tagger != null
     128           2 :           && (!user.get().isIdentifiedUser()
     129           2 :               || !user.get().asIdentifiedUser().hasEmailAddress(tagger.getEmailAddress()))) {
     130           0 :         perm.check(RefPermission.FORGE_COMMITTER);
     131             :       }
     132             : 
     133           2 :       RevObject target = tag.getObject();
     134           2 :       if (target instanceof RevCommit) {
     135           2 :         checkCreateCommit(user, repo, (RevCommit) target, ps.getNameKey(), perm, forPush);
     136             :       } else {
     137           0 :         checkCreateRef(user, repo, destBranch, target, forPush);
     138             :       }
     139             : 
     140             :       // If the tag has a PGP signature, allow a lower level of permission
     141             :       // than if it doesn't have a PGP signature.
     142           2 :       PermissionBackend.ForRef forRef = permissionBackend.user(user.get()).ref(destBranch);
     143           2 :       if (tag.getRawGpgSignature() != null) {
     144           0 :         forRef.check(RefPermission.CREATE_SIGNED_TAG);
     145             :       } else {
     146           2 :         forRef.check(RefPermission.CREATE_TAG);
     147             :       }
     148             :     }
     149          34 :   }
     150             : 
     151             :   /**
     152             :    * Check if the user is allowed to create a new commit object if this creation would introduce a
     153             :    * new commit to the repository.
     154             :    */
     155             :   private void checkCreateCommit(
     156             :       Provider<? extends CurrentUser> user,
     157             :       Repository repo,
     158             :       RevCommit commit,
     159             :       Project.NameKey project,
     160             :       PermissionBackend.ForRef forRef,
     161             :       boolean forPush)
     162             :       throws AuthException, PermissionBackendException, IOException {
     163             :     try {
     164             :       // If the user has UPDATE (push) permission, they can set the ref to an arbitrary commit:
     165             :       //
     166             :       //  * if they don't have access, we don't advertise the data, and a conforming git client
     167             :       //  would send the object along with the push as outcome of the negotation.
     168             :       //  * a malicious client could try to send the update without sending the object. This
     169             :       //  is prevented by JGit's ConnectivityChecker (see receive.checkReferencedObjectsAreReachable
     170             :       //  to switch off this costly check).
     171             :       //
     172             :       // Thus, when using the git command-line client, we don't need to do extra checks for users
     173             :       // with push access.
     174             :       //
     175             :       // When using the REST API, there is no negotiation, and the target commit must already be on
     176             :       // the server, so we must check that the user can see that commit.
     177          34 :       if (forPush) {
     178             :         // We can only shortcut for UPDATE permission. Pushing a tag (CREATE_TAG, CREATE_SIGNED_TAG)
     179             :         // can also introduce new objects. While there may not be a confidentiality problem
     180             :         // (the caller supplies the data as documented above), the permission is for creating
     181             :         // tags to existing commits.
     182          29 :         forRef.check(RefPermission.UPDATE);
     183          29 :         return;
     184             :       }
     185           6 :     } catch (AuthException denied) {
     186             :       // Fall through to check reachability.
     187          13 :     }
     188          19 :     if (reachable.fromRefs(
     189             :         project,
     190             :         repo,
     191             :         commit,
     192          19 :         repo.getRefDatabase().getRefsByPrefix(Constants.R_HEADS, Constants.R_TAGS),
     193          19 :         Optional.of(user.get()))) {
     194             :       // If the user has no push permissions, check whether the object is
     195             :       // merged into a branch or tag readable by this user. If so, they are
     196             :       // not effectively "pushing" more objects, so they can create the ref
     197             :       // even if they don't have push permission.
     198          17 :       return;
     199             :     }
     200             : 
     201             :     // Previous check only catches normal branches. Try PatchSet refs too. If we can create refs,
     202             :     // we're not a replica, so we can always use the change index.
     203          12 :     List<ChangeData> changes =
     204             :         retryHelper
     205          12 :             .changeIndexQuery(
     206             :                 "queryChangesByProjectCommitWithLimit1",
     207          12 :                 q -> q.enforceVisibility(true).setLimit(1).byProjectCommit(project, commit))
     208          12 :             .call();
     209          12 :     if (!changes.isEmpty()) {
     210           6 :       return;
     211             :     }
     212             : 
     213           6 :     AuthException e =
     214             :         new AuthException(
     215           6 :             String.format(
     216             :                 "%s for creating new commit object not permitted",
     217           6 :                 RefPermission.UPDATE.describeForException()));
     218           6 :     e.setAdvice(
     219           6 :         String.format(
     220             :             "use a SHA1 visible to you, or get %s permission on the ref",
     221           6 :             RefPermission.UPDATE.describeForException()));
     222           6 :     throw e;
     223             :   }
     224             : }

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