| // 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; | |
| } | |
| } |