| // Copyright (C) 2017 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.project; | 
 |  | 
 | import static com.google.gerrit.server.project.ProjectCache.noSuchProject; | 
 |  | 
 | import com.google.common.flogger.FluentLogger; | 
 | import com.google.gerrit.entities.BranchNameKey; | 
 | import com.google.gerrit.entities.Project; | 
 | import com.google.gerrit.extensions.restapi.AuthException; | 
 | import com.google.gerrit.extensions.restapi.ResourceConflictException; | 
 | import com.google.gerrit.server.CurrentUser; | 
 | import com.google.gerrit.server.permissions.PermissionBackend; | 
 | import com.google.gerrit.server.permissions.PermissionBackendException; | 
 | import com.google.gerrit.server.permissions.RefPermission; | 
 | import com.google.gerrit.server.query.change.ChangeData; | 
 | import com.google.gerrit.server.update.RetryHelper; | 
 | import com.google.inject.Inject; | 
 | import com.google.inject.Provider; | 
 | import com.google.inject.Singleton; | 
 | import java.io.IOException; | 
 | import java.util.List; | 
 | import java.util.Optional; | 
 | import org.eclipse.jgit.lib.Constants; | 
 | import org.eclipse.jgit.lib.PersonIdent; | 
 | import org.eclipse.jgit.lib.Repository; | 
 | import org.eclipse.jgit.revwalk.RevCommit; | 
 | import org.eclipse.jgit.revwalk.RevObject; | 
 | import org.eclipse.jgit.revwalk.RevTag; | 
 | import org.eclipse.jgit.revwalk.RevWalk; | 
 |  | 
 | /** Manages access control for creating Git references (aka branches, tags). */ | 
 | @Singleton | 
 | public class CreateRefControl { | 
 |  | 
 |   private static final FluentLogger logger = FluentLogger.forEnclosingClass(); | 
 |  | 
 |   private final PermissionBackend permissionBackend; | 
 |   private final ProjectCache projectCache; | 
 |   private final Reachable reachable; | 
 |   private final RetryHelper retryHelper; | 
 |  | 
 |   @Inject | 
 |   CreateRefControl( | 
 |       PermissionBackend permissionBackend, | 
 |       ProjectCache projectCache, | 
 |       Reachable reachable, | 
 |       RetryHelper retryHelper) { | 
 |     this.permissionBackend = permissionBackend; | 
 |     this.projectCache = projectCache; | 
 |     this.reachable = reachable; | 
 |     this.retryHelper = retryHelper; | 
 |   } | 
 |  | 
 |   /** | 
 |    * Checks whether the {@link CurrentUser} can create a new Git ref. | 
 |    * | 
 |    * @param user the user performing the operation | 
 |    * @param repo repository on which user want to create | 
 |    * @param destBranch the branch the new {@link RevObject} should be created on | 
 |    * @param object the object the user will start the reference with | 
 |    * @param sourceBranches the source ref from which the new ref is created from | 
 |    * @throws AuthException if creation is denied; the message explains the denial. | 
 |    * @throws PermissionBackendException on failure of permission checks. | 
 |    * @throws ResourceConflictException if the project state does not permit the operation | 
 |    */ | 
 |   public void checkCreateRef( | 
 |       Provider<? extends CurrentUser> user, | 
 |       Repository repo, | 
 |       BranchNameKey destBranch, | 
 |       RevObject object, | 
 |       boolean forPush, | 
 |       BranchNameKey... sourceBranches) | 
 |       throws AuthException, PermissionBackendException, NoSuchProjectException, IOException, | 
 |           ResourceConflictException { | 
 |     ProjectState ps = | 
 |         projectCache.get(destBranch.project()).orElseThrow(noSuchProject(destBranch.project())); | 
 |     ps.checkStatePermitsWrite(); | 
 |  | 
 |     PermissionBackend.ForRef perm = permissionBackend.user(user.get()).ref(destBranch); | 
 |     if (object instanceof RevCommit) { | 
 |       perm.check(RefPermission.CREATE); | 
 |       if (sourceBranches.length == 0) { | 
 |         checkCreateCommit(user, repo, (RevCommit) object, ps.getNameKey(), perm, forPush); | 
 |       } else { | 
 |         for (BranchNameKey src : sourceBranches) { | 
 |           PermissionBackend.ForRef forRef = permissionBackend.user(user.get()).ref(src); | 
 |           if (forRef.testOrFalse(RefPermission.READ)) { | 
 |             return; | 
 |           } | 
 |         } | 
 |         AuthException e = | 
 |             new AuthException( | 
 |                 String.format( | 
 |                     "must have %s on existing ref to create new ref from it", | 
 |                     RefPermission.READ.describeForException())); | 
 |         e.setAdvice( | 
 |             String.format( | 
 |                 "use an existing ref visible to you, or get %s permission on the ref", | 
 |                 RefPermission.READ.describeForException())); | 
 |         throw e; | 
 |       } | 
 |     } else if (object instanceof RevTag) { | 
 |       RevTag tag = (RevTag) object; | 
 |       try (RevWalk rw = new RevWalk(repo)) { | 
 |         rw.parseBody(tag); | 
 |       } catch (IOException e) { | 
 |         logger.atSevere().withCause(e).log( | 
 |             "RevWalk(%s) parsing %s:", destBranch.project(), tag.name()); | 
 |         throw e; | 
 |       } | 
 |  | 
 |       // If tagger is present, require it matches the user's email. | 
 |       PersonIdent tagger = tag.getTaggerIdent(); | 
 |       if (tagger != null | 
 |           && (!user.get().isIdentifiedUser() | 
 |               || !user.get().asIdentifiedUser().hasEmailAddress(tagger.getEmailAddress()))) { | 
 |         perm.check(RefPermission.FORGE_COMMITTER); | 
 |       } | 
 |  | 
 |       RevObject target = tag.getObject(); | 
 |       if (target instanceof RevCommit) { | 
 |         checkCreateCommit(user, repo, (RevCommit) target, ps.getNameKey(), perm, forPush); | 
 |       } else { | 
 |         checkCreateRef(user, repo, destBranch, target, forPush); | 
 |       } | 
 |  | 
 |       // If the tag has a PGP signature, allow a lower level of permission | 
 |       // than if it doesn't have a PGP signature. | 
 |       PermissionBackend.ForRef forRef = permissionBackend.user(user.get()).ref(destBranch); | 
 |       if (tag.getRawGpgSignature() != null) { | 
 |         forRef.check(RefPermission.CREATE_SIGNED_TAG); | 
 |       } else { | 
 |         forRef.check(RefPermission.CREATE_TAG); | 
 |       } | 
 |     } | 
 |   } | 
 |  | 
 |   /** | 
 |    * Check if the user is allowed to create a new commit object if this creation would introduce a | 
 |    * new commit to the repository. | 
 |    */ | 
 |   private void checkCreateCommit( | 
 |       Provider<? extends CurrentUser> user, | 
 |       Repository repo, | 
 |       RevCommit commit, | 
 |       Project.NameKey project, | 
 |       PermissionBackend.ForRef forRef, | 
 |       boolean forPush) | 
 |       throws AuthException, PermissionBackendException, IOException { | 
 |     try { | 
 |       // If the user has UPDATE (push) permission, they can set the ref to an arbitrary commit: | 
 |       // | 
 |       //  * if they don't have access, we don't advertise the data, and a conforming git client | 
 |       //  would send the object along with the push as outcome of the negotation. | 
 |       //  * a malicious client could try to send the update without sending the object. This | 
 |       //  is prevented by JGit's ConnectivityChecker (see receive.checkReferencedObjectsAreReachable | 
 |       //  to switch off this costly check). | 
 |       // | 
 |       // Thus, when using the git command-line client, we don't need to do extra checks for users | 
 |       // with push access. | 
 |       // | 
 |       // When using the REST API, there is no negotiation, and the target commit must already be on | 
 |       // the server, so we must check that the user can see that commit. | 
 |       if (forPush) { | 
 |         // We can only shortcut for UPDATE permission. Pushing a tag (CREATE_TAG, CREATE_SIGNED_TAG) | 
 |         // can also introduce new objects. While there may not be a confidentiality problem | 
 |         // (the caller supplies the data as documented above), the permission is for creating | 
 |         // tags to existing commits. | 
 |         forRef.check(RefPermission.UPDATE); | 
 |         return; | 
 |       } | 
 |     } catch (AuthException denied) { | 
 |       // Fall through to check reachability. | 
 |     } | 
 |     if (reachable.fromRefs( | 
 |         project, | 
 |         repo, | 
 |         commit, | 
 |         repo.getRefDatabase().getRefsByPrefix(Constants.R_HEADS, Constants.R_TAGS), | 
 |         Optional.of(user.get()))) { | 
 |       // If the user has no push permissions, check whether the object is | 
 |       // merged into a branch or tag readable by this user. If so, they are | 
 |       // not effectively "pushing" more objects, so they can create the ref | 
 |       // even if they don't have push permission. | 
 |       return; | 
 |     } | 
 |  | 
 |     // Previous check only catches normal branches. Try PatchSet refs too. If we can create refs, | 
 |     // we're not a replica, so we can always use the change index. | 
 |     List<ChangeData> changes = | 
 |         retryHelper | 
 |             .changeIndexQuery( | 
 |                 "queryChangesByProjectCommitWithLimit1", | 
 |                 q -> q.enforceVisibility(true).setLimit(1).byProjectCommit(project, commit)) | 
 |             .call(); | 
 |     if (!changes.isEmpty()) { | 
 |       return; | 
 |     } | 
 |  | 
 |     AuthException e = | 
 |         new AuthException( | 
 |             String.format( | 
 |                 "%s for creating new commit object not permitted", | 
 |                 RefPermission.UPDATE.describeForException())); | 
 |     e.setAdvice( | 
 |         String.format( | 
 |             "use a SHA1 visible to you, or get %s permission on the ref", | 
 |             RefPermission.UPDATE.describeForException())); | 
 |     throw e; | 
 |   } | 
 | } |