| // 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.collect.ImmutableList; |
| 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.extensions.restapi.UnprocessableEntityException; |
| 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.ArrayList; |
| import java.util.Arrays; |
| 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.Ref; |
| 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 UnprocessableEntityException if creation depends on an object or ref that is not |
| * visible |
| * @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, |
| UnprocessableEntityException { |
| 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 { |
| List<Ref> sourceRefs = new ArrayList<>(); |
| for (BranchNameKey src : sourceBranches) { |
| sourceRefs.add(repo.exactRef(src.branch())); |
| } |
| if (reachable.fromRefs( |
| destBranch.project(), |
| repo, |
| (RevCommit) object, |
| ImmutableList.copyOf(sourceRefs), |
| Optional.of(user.get()))) { |
| return; |
| } |
| // Don't expose existence of source refs or the object to the caller |
| throw new UnprocessableEntityException( |
| String.format( |
| "Unable to resolve object '%s'. Check that the provided source branch(es) exist" |
| + " (%s), that you are permitted to read them, and that the object is reachable" |
| + " from the source(s).", |
| object, Arrays.toString(sourceBranches))); |
| } |
| return; |
| } |
| 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); |
| } |
| return; |
| } |
| throw new AuthException( |
| String.format( |
| "Ref creation not allowed. Object %s is neither Commit or Tag.", object.getId())); |
| } |
| |
| /** |
| * 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 PermissionBackendException, IOException, UnprocessableEntityException { |
| // 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). |
| // |
| // 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. |
| // |
| // Thus, when using the git command-line client, we don't need to do extra checks for users |
| // with push access. |
| // |
| // 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. |
| if (forPush && forRef.test(RefPermission.UPDATE)) { |
| return; |
| } |
| // 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; |
| } |
| |
| // Don't expose existence of the commit to the caller |
| String msg = |
| String.format( |
| "Unable to resolve object '%s'. Check that the object exists on the server ", commit); |
| if (forPush) { |
| msg += |
| String.format( |
| "or get %s permission to create new commit objects.", |
| RefPermission.UPDATE.describeForException()); |
| } else { |
| msg += "and is visible to you."; |
| } |
| throw new UnprocessableEntityException(msg); |
| } |
| } |