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 : }
|