Line data Source code
1 : // Copyright (C) 2010 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.permissions;
16 :
17 : import static com.google.common.base.Preconditions.checkArgument;
18 :
19 : import com.google.common.collect.ImmutableList;
20 : import com.google.common.flogger.FluentLogger;
21 : import com.google.gerrit.common.Nullable;
22 : import com.google.gerrit.entities.Change;
23 : import com.google.gerrit.entities.Permission;
24 : import com.google.gerrit.entities.PermissionRange;
25 : import com.google.gerrit.entities.PermissionRule;
26 : import com.google.gerrit.entities.PermissionRule.Action;
27 : import com.google.gerrit.entities.Project;
28 : import com.google.gerrit.entities.RefNames;
29 : import com.google.gerrit.exceptions.StorageException;
30 : import com.google.gerrit.extensions.conditions.BooleanCondition;
31 : import com.google.gerrit.extensions.restapi.AuthException;
32 : import com.google.gerrit.server.CurrentUser;
33 : import com.google.gerrit.server.git.GitRepositoryManager;
34 : import com.google.gerrit.server.logging.CallerFinder;
35 : import com.google.gerrit.server.logging.LoggingContext;
36 : import com.google.gerrit.server.notedb.ChangeNotes;
37 : import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
38 : import com.google.gerrit.server.permissions.PermissionBackend.ForRef;
39 : import com.google.gerrit.server.query.change.ChangeData;
40 : import com.google.gerrit.server.util.MagicBranch;
41 : import java.io.IOException;
42 : import java.util.Collection;
43 : import java.util.EnumSet;
44 : import java.util.List;
45 : import java.util.Set;
46 : import java.util.concurrent.TimeUnit;
47 : import org.eclipse.jgit.lib.Constants;
48 : import org.eclipse.jgit.lib.Ref;
49 : import org.eclipse.jgit.lib.Repository;
50 :
51 : /** Manages access control for Git references (aka branches, tags). */
52 : class RefControl {
53 145 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
54 :
55 : private final ChangeData.Factory changeDataFactory;
56 : private final RefVisibilityControl refVisibilityControl;
57 : private final ProjectControl projectControl;
58 : private final GitRepositoryManager repositoryManager;
59 : private final String refName;
60 :
61 : /** All permissions that apply to this reference. */
62 : private final PermissionCollection relevant;
63 :
64 : private final CallerFinder callerFinder;
65 :
66 : // The next 4 members are cached canPerform() permissions.
67 :
68 : private Boolean owner;
69 : private Boolean canForgeAuthor;
70 : private Boolean canForgeCommitter;
71 : private Boolean hasReadPermissionOnRef;
72 :
73 : RefControl(
74 : ChangeData.Factory changeDataFactory,
75 : RefVisibilityControl refVisibilityControl,
76 : ProjectControl projectControl,
77 : GitRepositoryManager repositoryManager,
78 : String ref,
79 145 : PermissionCollection relevant) {
80 145 : this.changeDataFactory = changeDataFactory;
81 145 : this.refVisibilityControl = refVisibilityControl;
82 145 : this.projectControl = projectControl;
83 145 : this.repositoryManager = repositoryManager;
84 145 : this.refName = ref;
85 145 : this.relevant = relevant;
86 145 : this.callerFinder =
87 145 : CallerFinder.builder()
88 145 : .addTarget(PermissionBackend.class)
89 145 : .matchSubClasses(true)
90 145 : .matchInnerClasses(true)
91 145 : .skip(1)
92 145 : .build();
93 145 : }
94 :
95 : ProjectControl getProjectControl() {
96 103 : return projectControl;
97 : }
98 :
99 : CurrentUser getUser() {
100 132 : return projectControl.getUser();
101 : }
102 :
103 : /** Is this user a ref owner? */
104 : boolean isOwner() {
105 35 : if (owner == null) {
106 35 : if (canPerform(Permission.OWNER)) {
107 3 : owner = true;
108 :
109 : } else {
110 35 : owner = projectControl.isOwner();
111 : }
112 : }
113 35 : return owner;
114 : }
115 :
116 : /**
117 : * Returns {@code true} if the user has permission to read the ref. This method evaluates {@link
118 : * RefPermission#READ} only. Hence, it is not authoritative. For example, it does not tell if the
119 : * user can see NoteDb refs such as {@code refs/meta/external-ids} which requires {@link
120 : * GlobalPermission#ACCESS_DATABASE} and deny access in this case.
121 : */
122 : boolean hasReadPermissionOnRef(boolean allowNoteDbRefs) {
123 : // Don't allow checking for NoteDb refs unless instructed otherwise.
124 127 : if (!allowNoteDbRefs
125 126 : && (refName.startsWith(Constants.R_TAGS) || RefNames.isGerritRef(refName))) {
126 0 : logger.atWarning().atMostEvery(30, TimeUnit.SECONDS).log(
127 : "%s: Can't determine visibility of %s in RefControl. Denying access. "
128 : + "This case should have been handled before.",
129 0 : projectControl.getProject().getName(), refName);
130 0 : return false;
131 : }
132 :
133 127 : if (hasReadPermissionOnRef == null) {
134 127 : hasReadPermissionOnRef = getUser().isInternalUser() || canPerform(Permission.READ);
135 : }
136 127 : return hasReadPermissionOnRef;
137 : }
138 :
139 : /** Returns true if this user can add a new patch set to this ref */
140 : boolean canAddPatchSet() {
141 22 : return projectControl
142 22 : .controlForRef(MagicBranch.NEW_CHANGE + refName)
143 22 : .canPerform(Permission.ADD_PATCH_SET);
144 : }
145 :
146 : /** Returns true if this user can rebase changes on this ref */
147 : boolean canRebase() {
148 14 : return canPerform(Permission.REBASE);
149 : }
150 :
151 : /** Returns true if this user can submit patch sets to this ref */
152 : boolean canSubmit(boolean isChangeOwner) {
153 59 : if (RefNames.REFS_CONFIG.equals(refName)) {
154 : // Always allow project owners to submit configuration changes.
155 : // Submitting configuration changes modifies the access control
156 : // rules. Allowing this to be done by a non-project-owner opens
157 : // a security hole enabling editing of access rules, and thus
158 : // granting of powers beyond submitting to the configuration.
159 5 : return projectControl.isOwner();
160 : }
161 59 : return canPerform(Permission.SUBMIT, isChangeOwner, false);
162 : }
163 :
164 : /** Returns true if this user can force edit topic names. */
165 : boolean canForceEditTopicName() {
166 26 : return canPerform(Permission.EDIT_TOPIC_NAME, false, true);
167 : }
168 :
169 : /** Returns true if this user can delete changes. */
170 : boolean canDeleteChanges(boolean isChangeOwner) {
171 22 : return canPerform(Permission.DELETE_CHANGES)
172 22 : || (isChangeOwner && canPerform(Permission.DELETE_OWN_CHANGES, isChangeOwner, false));
173 : }
174 :
175 : /** The range of permitted values associated with a label permission. */
176 : PermissionRange getRange(String permission) {
177 1 : return getRange(permission, false);
178 : }
179 :
180 : /** The range of permitted values associated with a label permission. */
181 : @Nullable
182 : PermissionRange getRange(String permission, boolean isChangeOwner) {
183 103 : if (Permission.hasRange(permission)) {
184 103 : return toRange(permission, isChangeOwner);
185 : }
186 0 : return null;
187 : }
188 :
189 : /** True if the user has this permission. Works only for non labels. */
190 : boolean canPerform(String permissionName) {
191 145 : return canPerform(permissionName, false, false);
192 : }
193 :
194 : ForRef asForRef() {
195 132 : return new ForRefImpl();
196 : }
197 :
198 : private boolean canUpload() {
199 104 : return projectControl.controlForRef("refs/for/" + refName).canPerform(Permission.PUSH);
200 : }
201 :
202 : boolean canRevert() {
203 27 : return canPerform(Permission.REVERT);
204 : }
205 :
206 : /** Returns true if this user can submit merge patch sets to this ref */
207 : private boolean canUploadMerges() {
208 26 : return projectControl.controlForRef("refs/for/" + refName).canPerform(Permission.PUSH_MERGE);
209 : }
210 :
211 : /** Returns true if the user can update the reference as a fast-forward. */
212 : private boolean canUpdate() {
213 48 : if (RefNames.REFS_CONFIG.equals(refName) && !projectControl.isOwner()) {
214 : // Pushing requires being at least project owner, in addition to push.
215 : // Pushing configuration changes modifies the access control
216 : // rules. Allowing this to be done by a non-project-owner opens
217 : // a security hole enabling editing of access rules, and thus
218 : // granting of powers beyond pushing to the configuration.
219 :
220 : // On the AllProjects project the owner access right cannot be assigned,
221 : // this why for the AllProjects project we allow administrators to push
222 : // configuration changes if they have push without being project owner.
223 1 : if (!(projectControl.getProjectState().isAllProjects() && projectControl.isAdmin())) {
224 1 : return false;
225 : }
226 : }
227 48 : return canPerform(Permission.PUSH);
228 : }
229 :
230 : /** Returns true if the user can rewind (force push) the reference. */
231 : private boolean canForceUpdate() {
232 13 : if (canPushWithForce()) {
233 12 : return true;
234 : }
235 :
236 8 : switch (getUser().getAccessPath()) {
237 : case GIT:
238 7 : return false;
239 :
240 : case REST_API:
241 : case SSH_COMMAND:
242 : case UNKNOWN:
243 : case WEB_BROWSER:
244 : default:
245 1 : return (isOwner() && !isBlocked(Permission.PUSH, false, true)) || projectControl.isAdmin();
246 : }
247 : }
248 :
249 : private boolean canPushWithForce() {
250 44 : if (RefNames.REFS_CONFIG.equals(refName) && !projectControl.isOwner()) {
251 : // Pushing requires being at least project owner, in addition to push.
252 : // Pushing configuration changes modifies the access control
253 : // rules. Allowing this to be done by a non-project-owner opens
254 : // a security hole enabling editing of access rules, and thus
255 : // granting of powers beyond pushing to the configuration.
256 0 : return false;
257 : }
258 44 : return canPerform(Permission.PUSH, false, true);
259 : }
260 :
261 : /**
262 : * Determines whether the user can delete the Git ref controlled by this object.
263 : *
264 : * @return {@code true} if the user specified can delete a Git ref.
265 : */
266 : private boolean canDelete() {
267 41 : switch (getUser().getAccessPath()) {
268 : case GIT:
269 11 : return canPushWithForce() || canPerform(Permission.DELETE);
270 :
271 : case REST_API:
272 : case SSH_COMMAND:
273 : case UNKNOWN:
274 : case WEB_BROWSER:
275 : default:
276 35 : return canPushWithForce() || canPerform(Permission.DELETE) || projectControl.isAdmin();
277 : }
278 : }
279 :
280 : /** Returns true if this user can forge the author line in a commit. */
281 : private boolean canForgeAuthor() {
282 36 : if (canForgeAuthor == null) {
283 36 : canForgeAuthor = canPerform(Permission.FORGE_AUTHOR);
284 : }
285 36 : return canForgeAuthor;
286 : }
287 :
288 : /** Returns true if this user can forge the committer line in a commit. */
289 : private boolean canForgeCommitter() {
290 30 : if (canForgeCommitter == null) {
291 30 : canForgeCommitter = canPerform(Permission.FORGE_COMMITTER);
292 : }
293 30 : return canForgeCommitter;
294 : }
295 :
296 : /** Returns true if this user can forge the server on the committer line. */
297 : private boolean canForgeGerritServerIdentity() {
298 5 : return canPerform(Permission.FORGE_SERVER);
299 : }
300 :
301 : private static boolean isAllow(PermissionRule pr, boolean withForce) {
302 145 : return pr.getAction() == Action.ALLOW && (pr.getForce() || !withForce);
303 : }
304 :
305 : private static boolean isBlock(PermissionRule pr, boolean withForce) {
306 : // BLOCK with force specified is a weaker rule than without.
307 49 : return pr.getAction() == Action.BLOCK && (!pr.getForce() || withForce);
308 : }
309 :
310 : private PermissionRange toRange(String permissionName, boolean isChangeOwner) {
311 103 : int blockAllowMin = Integer.MIN_VALUE, blockAllowMax = Integer.MAX_VALUE;
312 :
313 : projectLoop:
314 103 : for (List<Permission> ps : relevant.getBlockRules(permissionName)) {
315 3 : boolean blockFound = false;
316 3 : int projectBlockAllowMin = Integer.MIN_VALUE, projectBlockAllowMax = Integer.MAX_VALUE;
317 :
318 3 : for (Permission p : ps) {
319 3 : if (p.getExclusiveGroup()) {
320 1 : for (PermissionRule pr : p.getRules()) {
321 1 : if (pr.getAction() == Action.ALLOW && projectControl.match(pr, isChangeOwner)) {
322 : // exclusive override, usually for a more specific ref.
323 1 : continue projectLoop;
324 : }
325 0 : }
326 : }
327 :
328 3 : for (PermissionRule pr : p.getRules()) {
329 3 : if (pr.getAction() == Action.BLOCK && projectControl.match(pr, isChangeOwner)) {
330 3 : projectBlockAllowMin = pr.getMin() + 1;
331 3 : projectBlockAllowMax = pr.getMax() - 1;
332 3 : blockFound = true;
333 : }
334 3 : }
335 :
336 3 : if (blockFound) {
337 3 : for (PermissionRule pr : p.getRules()) {
338 3 : if (pr.getAction() == Action.ALLOW && projectControl.match(pr, isChangeOwner)) {
339 1 : projectBlockAllowMin = pr.getMin();
340 1 : projectBlockAllowMax = pr.getMax();
341 1 : break;
342 : }
343 3 : }
344 3 : break;
345 : }
346 2 : }
347 :
348 3 : blockAllowMin = Math.max(projectBlockAllowMin, blockAllowMin);
349 3 : blockAllowMax = Math.min(projectBlockAllowMax, blockAllowMax);
350 3 : }
351 :
352 103 : int voteMin = 0, voteMax = 0;
353 103 : for (PermissionRule pr : relevant.getAllowRules(permissionName)) {
354 103 : if (pr.getAction() == PermissionRule.Action.ALLOW
355 103 : && projectControl.match(pr, isChangeOwner)) {
356 : // For votes, contrary to normal permissions, we aggregate all applicable rules.
357 103 : voteMin = Math.min(voteMin, pr.getMin());
358 103 : voteMax = Math.max(voteMax, pr.getMax());
359 : }
360 103 : }
361 :
362 103 : return new PermissionRange(
363 : permissionName,
364 103 : /* min= */ Math.max(voteMin, blockAllowMin),
365 103 : /* max= */ Math.min(voteMax, blockAllowMax));
366 : }
367 :
368 : private boolean isBlocked(String permissionName, boolean isChangeOwner, boolean withForce) {
369 : // Permissions are ordered by (more general project, more specific ref). Because Permission
370 : // does not have back pointers, we can't tell what ref-pattern or project each permission comes
371 : // from.
372 145 : List<List<Permission>> downwardPerProject = relevant.getBlockRules(permissionName);
373 :
374 : projectLoop:
375 145 : for (List<Permission> projectRules : downwardPerProject) {
376 49 : boolean overrideFound = false;
377 49 : for (Permission p : projectRules) {
378 : // If this is an exclusive ALLOW, then block rules from the same project are ignored.
379 49 : if (p.getExclusiveGroup()) {
380 1 : for (PermissionRule pr : p.getRules()) {
381 1 : if (isAllow(pr, withForce) && projectControl.match(pr, isChangeOwner)) {
382 1 : overrideFound = true;
383 1 : break;
384 : }
385 0 : }
386 : }
387 49 : if (overrideFound) {
388 : // Found an exclusive override, nothing further to do in this project.
389 1 : continue projectLoop;
390 : }
391 :
392 49 : boolean blocked = false;
393 49 : for (PermissionRule pr : p.getRules()) {
394 49 : if (!withForce && pr.getForce()) {
395 : // force on block rule only applies to withForce permission.
396 0 : continue;
397 : }
398 :
399 49 : if (isBlock(pr, withForce) && projectControl.match(pr, isChangeOwner)) {
400 49 : blocked = true;
401 49 : break;
402 : }
403 26 : }
404 :
405 49 : if (blocked) {
406 : // ALLOW in the same AccessSection (ie. in the same Permission) overrides the BLOCK.
407 49 : for (PermissionRule pr : p.getRules()) {
408 49 : if (isAllow(pr, withForce) && projectControl.match(pr, isChangeOwner)) {
409 23 : blocked = false;
410 23 : break;
411 : }
412 46 : }
413 : }
414 :
415 49 : if (blocked) {
416 46 : return true;
417 : }
418 26 : }
419 26 : }
420 :
421 145 : return false;
422 : }
423 :
424 : /** True if the user has this permission. */
425 : private boolean canPerform(String permissionName, boolean isChangeOwner, boolean withForce) {
426 145 : if (isBlocked(permissionName, isChangeOwner, withForce)) {
427 46 : if (logger.atFine().isEnabled() || LoggingContext.getInstance().isAclLogging()) {
428 11 : String logMessage =
429 11 : String.format(
430 : "'%s' cannot perform '%s' with force=%s on project '%s' for ref '%s'"
431 : + " because this permission is blocked",
432 11 : getUser().getLoggableName(),
433 : permissionName,
434 11 : withForce,
435 11 : projectControl.getProject().getName(),
436 : refName);
437 11 : LoggingContext.getInstance().addAclLogRecord(logMessage);
438 11 : logger.atFine().log("%s (caller: %s)", logMessage, callerFinder.findCallerLazy());
439 : }
440 46 : return false;
441 : }
442 :
443 145 : for (PermissionRule pr : relevant.getAllowRules(permissionName)) {
444 145 : if (isAllow(pr, withForce) && projectControl.match(pr, isChangeOwner)) {
445 145 : if (logger.atFine().isEnabled() || LoggingContext.getInstance().isAclLogging()) {
446 22 : String logMessage =
447 22 : String.format(
448 : "'%s' can perform '%s' with force=%s on project '%s' for ref '%s'",
449 22 : getUser().getLoggableName(),
450 : permissionName,
451 22 : withForce,
452 22 : projectControl.getProject().getName(),
453 : refName);
454 22 : LoggingContext.getInstance().addAclLogRecord(logMessage);
455 22 : logger.atFine().log("%s (caller: %s)", logMessage, callerFinder.findCallerLazy());
456 : }
457 145 : return true;
458 : }
459 69 : }
460 :
461 117 : if (logger.atFine().isEnabled() || LoggingContext.getInstance().isAclLogging()) {
462 20 : String logMessage =
463 20 : String.format(
464 : "'%s' cannot perform '%s' with force=%s on project '%s' for ref '%s'",
465 20 : getUser().getLoggableName(),
466 : permissionName,
467 20 : withForce,
468 20 : projectControl.getProject().getName(),
469 : refName);
470 20 : LoggingContext.getInstance().addAclLogRecord(logMessage);
471 20 : logger.atFine().log("%s (caller: %s)", logMessage, callerFinder.findCallerLazy());
472 : }
473 117 : return false;
474 : }
475 :
476 132 : private class ForRefImpl extends ForRef {
477 : private String resourcePath;
478 :
479 : @Override
480 : public String resourcePath() {
481 58 : if (resourcePath == null) {
482 58 : resourcePath =
483 58 : String.format(
484 58 : "/projects/%s/+refs/%s", getProjectControl().getProjectState().getName(), refName);
485 : }
486 58 : return resourcePath;
487 : }
488 :
489 : @Override
490 : public ForChange change(ChangeData cd) {
491 : try {
492 103 : return getProjectControl().controlFor(cd).asForChange();
493 0 : } catch (StorageException e) {
494 0 : return FailedPermissionBackend.change("unavailable", e);
495 : }
496 : }
497 :
498 : @Override
499 : public ForChange change(ChangeNotes notes) {
500 103 : Project.NameKey project = getProjectControl().getProject().getNameKey();
501 103 : Change change = notes.getChange();
502 103 : checkArgument(
503 103 : project.equals(change.getProject()),
504 : "expected change in project %s, not %s",
505 : project,
506 103 : change.getProject());
507 : // Having ChangeNotes means it's OK to load values from NoteDb if needed.
508 : // ChangeData.Factory will allow lazyLoading
509 103 : return getProjectControl().controlFor(changeDataFactory.create(notes)).asForChange();
510 : }
511 :
512 : @Override
513 : public void check(RefPermission perm) throws AuthException, PermissionBackendException {
514 118 : if (!can(perm)) {
515 25 : PermissionDeniedException pde = new PermissionDeniedException(perm, refName);
516 25 : switch (perm) {
517 : case UPDATE:
518 6 : if (refName.equals(RefNames.REFS_CONFIG)) {
519 1 : pde.setAdvice(
520 : "Configuration changes can only be pushed by project owners\n"
521 : + "who also have 'Push' rights on "
522 : + RefNames.REFS_CONFIG);
523 : } else {
524 6 : pde.setAdvice(
525 : "Push to refs/for/"
526 6 : + RefNames.shortName(refName)
527 : + " to create a review, or get 'Push' rights to update the branch.");
528 : }
529 6 : break;
530 : case DELETE:
531 11 : pde.setAdvice(
532 : "You need 'Delete Reference' rights or 'Push' rights with the \n"
533 : + "'Force Push' flag set to delete references.");
534 11 : break;
535 : case CREATE_CHANGE:
536 : // This is misleading in the default permission backend, since "create change" on a
537 : // branch is encoded as "push" on refs/for/DESTINATION.
538 3 : pde.setAdvice(
539 : "You need 'Create Change' rights to upload code review requests.\n"
540 : + "Verify that you are pushing to the right branch.");
541 3 : break;
542 : case CREATE:
543 8 : pde.setAdvice("You need 'Create' rights to create new references.");
544 8 : break;
545 : case CREATE_SIGNED_TAG:
546 0 : pde.setAdvice("You need 'Create Signed Tag' rights to push a signed tag.");
547 0 : break;
548 : case CREATE_TAG:
549 3 : pde.setAdvice("You need 'Create Tag' rights to push a normal tag.");
550 3 : break;
551 : case FORCE_UPDATE:
552 7 : pde.setAdvice(
553 : "You need 'Push' rights with 'Force' flag set to do a non-fastforward push.");
554 7 : break;
555 : case FORGE_AUTHOR:
556 2 : pde.setAdvice(
557 : "You need 'Forge Author' rights to push commits with another user as author.");
558 2 : break;
559 : case FORGE_COMMITTER:
560 0 : pde.setAdvice(
561 : "You need 'Forge Committer' rights to push commits with another user as"
562 : + " committer.");
563 0 : break;
564 : case FORGE_SERVER:
565 0 : pde.setAdvice(
566 : "You need 'Forge Server' rights to push merge commits authored by the server.");
567 0 : break;
568 : case MERGE:
569 0 : pde.setAdvice(
570 : "You need 'Push Merge' in addition to 'Push' rights to push merge commits.");
571 0 : break;
572 :
573 : case READ:
574 5 : pde.setAdvice("You need 'Read' rights to fetch or clone this ref.");
575 5 : break;
576 :
577 : case READ_CONFIG:
578 0 : pde.setAdvice("You need 'Read' rights on refs/meta/config to see the configuration.");
579 0 : break;
580 : case READ_PRIVATE_CHANGES:
581 1 : pde.setAdvice("You need 'Read Private Changes' to see private changes.");
582 1 : break;
583 : case SET_HEAD:
584 1 : pde.setAdvice("You need 'Set HEAD' rights to set the default branch.");
585 1 : break;
586 : case SKIP_VALIDATION:
587 4 : pde.setAdvice(
588 : "You need 'Forge Author', 'Forge Server', 'Forge Committer'\n"
589 : + "and 'Push Merge' rights to skip validation.");
590 4 : break;
591 : case UPDATE_BY_SUBMIT:
592 3 : pde.setAdvice(
593 : "You need 'Submit' rights on refs/for/ to submit changes during change upload.");
594 3 : break;
595 :
596 : case WRITE_CONFIG:
597 1 : pde.setAdvice("You need 'Write' rights on refs/meta/config.");
598 : break;
599 : }
600 25 : throw pde;
601 : }
602 118 : }
603 :
604 : @Override
605 : public Set<RefPermission> test(Collection<RefPermission> permSet)
606 : throws PermissionBackendException {
607 129 : EnumSet<RefPermission> ok = EnumSet.noneOf(RefPermission.class);
608 129 : for (RefPermission perm : permSet) {
609 129 : if (can(perm)) {
610 126 : ok.add(perm);
611 : }
612 129 : }
613 129 : return ok;
614 : }
615 :
616 : @Override
617 : public BooleanCondition testCond(RefPermission perm) {
618 58 : return new PermissionBackendCondition.ForRef(this, perm, getUser());
619 : }
620 :
621 : private boolean can(RefPermission perm) throws PermissionBackendException {
622 132 : switch (perm) {
623 : case READ:
624 : /* Internal users such as plugin users should be able to read all refs. */
625 131 : if (getUser().isInternalUser()) {
626 5 : return true;
627 : }
628 131 : if (refName.startsWith(Constants.R_TAGS)) {
629 12 : return isTagVisible();
630 : }
631 130 : return refVisibilityControl.isVisible(projectControl, refName);
632 : case CREATE:
633 : // TODO This isn't an accurate test.
634 43 : return canPerform(refPermissionName(perm));
635 : case DELETE:
636 41 : return canDelete();
637 : case UPDATE:
638 48 : return canUpdate();
639 : case FORCE_UPDATE:
640 13 : return canForceUpdate();
641 : case SET_HEAD:
642 1 : return projectControl.isOwner();
643 :
644 : case FORGE_AUTHOR:
645 36 : return canForgeAuthor();
646 : case FORGE_COMMITTER:
647 30 : return canForgeCommitter();
648 : case FORGE_SERVER:
649 1 : return canForgeGerritServerIdentity();
650 : case MERGE:
651 26 : return canUploadMerges();
652 :
653 : case CREATE_CHANGE:
654 104 : return canUpload();
655 :
656 : case CREATE_TAG:
657 : case CREATE_SIGNED_TAG:
658 4 : return canPerform(refPermissionName(perm));
659 :
660 : case UPDATE_BY_SUBMIT:
661 9 : return projectControl.controlForRef(MagicBranch.NEW_CHANGE + refName).canSubmit(true);
662 :
663 : case READ_PRIVATE_CHANGES:
664 12 : return canPerform(Permission.VIEW_PRIVATE_CHANGES);
665 :
666 : case READ_CONFIG:
667 0 : return projectControl
668 0 : .controlForRef(RefNames.REFS_CONFIG)
669 0 : .canPerform(RefPermission.READ.name());
670 : case WRITE_CONFIG:
671 29 : return isOwner();
672 :
673 : case SKIP_VALIDATION:
674 4 : return canForgeAuthor()
675 4 : && canForgeCommitter()
676 4 : && canForgeGerritServerIdentity()
677 4 : && canUploadMerges();
678 : }
679 0 : throw new PermissionBackendException(perm + " unsupported");
680 : }
681 :
682 : private boolean isTagVisible() throws PermissionBackendException {
683 12 : if (projectControl.asForProject().test(ProjectPermission.READ)) {
684 : // The user has READ on refs/* with no effective block permission. This is the broadest
685 : // permission one can assign. There is no way to grant access to (specific) tags in Gerrit,
686 : // so we have to assume that these users can see all tags because there could be tags that
687 : // aren't reachable by any visible ref while the user can see all non-Gerrit refs. This
688 : // matches Gerrit's historic behavior.
689 : // This makes it so that these users could see commits that they can't see otherwise
690 : // (e.g. a private change ref) if a tag was attached to it. Tags are meant to be used on
691 : // the regular Git tree that users interact with, not on any of the Gerrit trees, so this
692 : // is a negligible risk.
693 12 : return true;
694 : }
695 :
696 5 : try (Repository repo =
697 5 : repositoryManager.openRepository(projectControl.getProject().getNameKey())) {
698 : // Tag visibility requires going through RefFilter because it entails loading all taggable
699 : // refs and filtering them all by visibility.
700 5 : Ref resolvedRef = repo.getRefDatabase().exactRef(refName);
701 5 : if (resolvedRef == null) {
702 0 : return false;
703 : }
704 5 : return projectControl.asForProject()
705 5 : .filter(
706 5 : ImmutableList.of(resolvedRef), repo, PermissionBackend.RefFilterOptions.defaults())
707 5 : .stream()
708 5 : .anyMatch(r -> refName.equals(r.getName()));
709 0 : } catch (IOException e) {
710 0 : throw new PermissionBackendException(e);
711 : }
712 : }
713 : }
714 :
715 : private static String refPermissionName(RefPermission refPermission) {
716 : // Within this class, it's programmer error to call this method on a
717 : // RefPermission that isn't associated with a permission name.
718 45 : return DefaultPermissionMappings.refPermissionName(refPermission)
719 45 : .orElseThrow(() -> new IllegalStateException("no name for " + refPermission));
720 : }
721 : }
|