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.permissions;
16 :
17 : import static java.util.Objects.requireNonNull;
18 : import static java.util.stream.Collectors.toSet;
19 :
20 : import com.google.auto.value.AutoValue;
21 : import com.google.common.collect.ImmutableList;
22 : import com.google.common.collect.Sets;
23 : import com.google.common.flogger.FluentLogger;
24 : import com.google.gerrit.entities.Account;
25 : import com.google.gerrit.entities.BranchNameKey;
26 : import com.google.gerrit.entities.LabelType;
27 : import com.google.gerrit.entities.Project;
28 : import com.google.gerrit.exceptions.StorageException;
29 : import com.google.gerrit.extensions.api.access.CoreOrPluginProjectPermission;
30 : import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission;
31 : import com.google.gerrit.extensions.conditions.BooleanCondition;
32 : import com.google.gerrit.extensions.restapi.AuthException;
33 : import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
34 : import com.google.gerrit.server.CurrentUser;
35 : import com.google.gerrit.server.notedb.ChangeNotes;
36 : import com.google.gerrit.server.query.change.ChangeData;
37 : import com.google.inject.ImplementedBy;
38 : import java.util.Collection;
39 : import java.util.Collections;
40 : import java.util.EnumSet;
41 : import java.util.Iterator;
42 : import java.util.List;
43 : import java.util.Set;
44 : import org.eclipse.jgit.errors.RepositoryNotFoundException;
45 : import org.eclipse.jgit.lib.Ref;
46 : import org.eclipse.jgit.lib.Repository;
47 :
48 : /**
49 : * Checks authorization to perform an action on a project, reference, or change.
50 : *
51 : * <p>{@code check} methods should be used during action handlers to verify the user is allowed to
52 : * exercise the specified permission. For convenience in implementation {@code check} methods throw
53 : * {@link AuthException} if the permission is denied.
54 : *
55 : * <p>{@code test} methods should be used when constructing replies to the client and the result
56 : * object needs to include a true/false hint indicating the user's ability to exercise the
57 : * permission. This is suitable for configuring UI button state, but should not be relied upon to
58 : * guard handlers before making state changes.
59 : *
60 : * <p>{@code PermissionBackend} is a singleton for the server, acting as a factory for lightweight
61 : * request instances. Implementation classes may cache supporting data inside of {@link WithUser},
62 : * {@link ForProject}, {@link ForRef}, and {@link ForChange} instances, in addition to storing
63 : * within {@link CurrentUser} using a {@link com.google.gerrit.server.PropertyMap.Key}. {@link
64 : * GlobalPermission} caching for {@link WithUser} may best cached inside {@link CurrentUser} as
65 : * {@link WithUser} instances are frequently created.
66 : *
67 : * <p>Example use:
68 : *
69 : * <pre>
70 : * private final PermissionBackend permissions;
71 : * private final Provider<CurrentUser> user;
72 : *
73 : * {@literal @}Inject
74 : * Foo(PermissionBackend permissions, Provider<CurrentUser> user) {
75 : * this.permissions = permissions;
76 : * this.user = user;
77 : * }
78 : *
79 : * public void apply(...) {
80 : * permissions.user(user).change(cd).check(ChangePermission.SUBMIT);
81 : * }
82 : *
83 : * public UiAction.Description getDescription(ChangeResource rsrc) {
84 : * return new UiAction.Description()
85 : * .setLabel("Submit")
86 : * .setVisible(rsrc.permissions().testCond(ChangePermission.SUBMIT));
87 : * }
88 : * </pre>
89 : */
90 : @ImplementedBy(DefaultPermissionBackend.class)
91 151 : public abstract class PermissionBackend {
92 151 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
93 :
94 : /** Returns an instance scoped to the current user. */
95 : public abstract WithUser currentUser();
96 :
97 : /**
98 : * Returns an instance scoped to the specified user. Should be used in cases where the user could
99 : * either be the issuer of the current request or an impersonated user. PermissionBackends that do
100 : * not support impersonation can fail with an {@code IllegalStateException}.
101 : *
102 : * <p>If an instance scoped to the current user is desired, use {@code currentUser()} instead.
103 : */
104 : public abstract WithUser user(CurrentUser user);
105 :
106 : /**
107 : * Returns an instance scoped to the provided user. Should be used in cases where the caller wants
108 : * to check the permissions of a user who is not the issuer of the current request and not the
109 : * target of impersonation.
110 : *
111 : * <p>Usage should be very limited as this can expose a group-oracle.
112 : */
113 : public abstract WithUser absentUser(Account.Id id);
114 :
115 : /**
116 : * Check whether this {@code PermissionBackend} respects the same global capabilities as the
117 : * {@link DefaultPermissionBackend}.
118 : *
119 : * <p>If true, then it makes sense for downstream callers to refer to built-in Gerrit capability
120 : * names in user-facing error messages, for example.
121 : *
122 : * @return whether this is the default permission backend.
123 : */
124 : public boolean usesDefaultCapabilities() {
125 0 : return false;
126 : }
127 :
128 : /**
129 : * Throw {@link ResourceNotFoundException} if this backend does not use the default global
130 : * capabilities.
131 : */
132 : public void checkUsesDefaultCapabilities() throws ResourceNotFoundException {
133 4 : if (!usesDefaultCapabilities()) {
134 0 : throw new ResourceNotFoundException("Gerrit capabilities not used on this server");
135 : }
136 4 : }
137 :
138 : /**
139 : * Bulk evaluate a set of {@link PermissionBackendCondition} for view handling.
140 : *
141 : * <p>Overridden implementations should call {@link PermissionBackendCondition#set(boolean)} to
142 : * cache the result of {@code testOrFalse} in the condition for later evaluation. Caching the
143 : * result will bypass the usual invocation of {@code testOrFalse}.
144 : *
145 : * @param conds conditions to consider.
146 : */
147 : public void bulkEvaluateTest(Set<PermissionBackendCondition> conds) {
148 : // Do nothing by default. The default implementation of PermissionBackendCondition
149 : // delegates to the appropriate testOrFalse method in PermissionBackend.
150 66 : }
151 :
152 : /** PermissionBackend scoped to a specific user. */
153 150 : public abstract static class WithUser {
154 : /** Returns an instance scoped for the specified project. */
155 : public abstract ForProject project(Project.NameKey project);
156 :
157 : /** Returns an instance scoped for the {@code ref}, and its parent project. */
158 : public ForRef ref(BranchNameKey ref) {
159 129 : return project(ref.project()).ref(ref.branch());
160 : }
161 :
162 : /** Returns an instance scoped for the change, and its destination ref and project. */
163 : public ForChange change(ChangeData cd) {
164 : try {
165 103 : return ref(cd.change().getDest()).change(cd);
166 0 : } catch (StorageException e) {
167 0 : return FailedPermissionBackend.change("unavailable", e);
168 : }
169 : }
170 :
171 : /** Returns an instance scoped for the change, and its destination ref and project. */
172 : public ForChange change(ChangeNotes notes) {
173 103 : return ref(notes.getChange().getDest()).change(notes);
174 : }
175 :
176 : /**
177 : * Verify scoped user can {@code perm}, throwing if denied.
178 : *
179 : * <p>Should be used in REST API handlers where the thrown {@link AuthException} can be
180 : * propagated. In business logic, where the exception would have to be caught, prefer using
181 : * {@link #test(GlobalOrPluginPermission)}.
182 : */
183 : public abstract void check(GlobalOrPluginPermission perm)
184 : throws AuthException, PermissionBackendException;
185 :
186 : /**
187 : * Verify scoped user can perform at least one listed permission.
188 : *
189 : * <p>If {@code any} is empty, the method completes normally and allows the caller to continue.
190 : * Since no permissions were supplied to check, its assumed no permissions are necessary to
191 : * continue with the caller's operation.
192 : *
193 : * <p>If the user has at least one of the permissions in {@code any}, the method completes
194 : * normally, possibly without checking all listed permissions.
195 : *
196 : * <p>If {@code any} is non-empty and the user has none, {@link AuthException} is thrown for one
197 : * of the failed permissions.
198 : *
199 : * @param any set of permissions to check.
200 : */
201 : public void checkAny(Set<GlobalOrPluginPermission> any)
202 : throws PermissionBackendException, AuthException {
203 148 : for (Iterator<GlobalOrPluginPermission> itr = any.iterator(); itr.hasNext(); ) {
204 : try {
205 148 : check(itr.next());
206 148 : return;
207 6 : } catch (AuthException err) {
208 6 : if (!itr.hasNext()) {
209 5 : throw err;
210 : }
211 3 : }
212 : }
213 36 : }
214 :
215 : /** Filter {@code permSet} to permissions scoped user might be able to perform. */
216 : public abstract <T extends GlobalOrPluginPermission> Set<T> test(Collection<T> permSet)
217 : throws PermissionBackendException;
218 :
219 : public boolean test(GlobalOrPluginPermission perm) throws PermissionBackendException {
220 147 : return test(Collections.singleton(perm)).contains(perm);
221 : }
222 :
223 : public boolean testOrFalse(GlobalOrPluginPermission perm) {
224 : try {
225 112 : return test(perm);
226 0 : } catch (PermissionBackendException e) {
227 0 : logger.atWarning().withCause(e).log("Cannot test %s; assuming false", perm);
228 0 : return false;
229 : }
230 : }
231 :
232 : public abstract BooleanCondition testCond(GlobalOrPluginPermission perm);
233 :
234 : /**
235 : * Filter a set of projects using {@code check(perm)}.
236 : *
237 : * @param perm required permission in a project to be included in result.
238 : * @param projects candidate set of projects; may be empty.
239 : * @return filtered set of {@code projects} where {@code check(perm)} was successful.
240 : * @throws PermissionBackendException backend cannot access its internal state.
241 : */
242 : public Set<Project.NameKey> filter(ProjectPermission perm, Collection<Project.NameKey> projects)
243 : throws PermissionBackendException {
244 6 : requireNonNull(perm, "ProjectPermission");
245 6 : requireNonNull(projects, "projects");
246 6 : Set<Project.NameKey> allowed = Sets.newHashSetWithExpectedSize(projects.size());
247 6 : for (Project.NameKey project : projects) {
248 : try {
249 6 : if (project(project).test(perm)) {
250 6 : allowed.add(project);
251 : }
252 0 : } catch (PermissionBackendException e) {
253 0 : if (e.getCause() instanceof RepositoryNotFoundException) {
254 0 : logger.atWarning().withCause(e).log(
255 0 : "Could not find repository of the project %s", project.get());
256 : // Do not include this project because doesn't exist
257 : } else {
258 0 : throw e;
259 : }
260 6 : }
261 6 : }
262 6 : return allowed;
263 : }
264 : }
265 :
266 : /** PermissionBackend scoped to a user and project. */
267 145 : public abstract static class ForProject {
268 : /** Returns the fully qualified resource path that this instance is scoped to. */
269 : public abstract String resourcePath();
270 :
271 : /** Returns an instance scoped for {@code ref} in this project. */
272 : public abstract ForRef ref(String ref);
273 :
274 : /** Returns an instance scoped for the change, and its destination ref and project. */
275 : public ForChange change(ChangeData cd) {
276 : try {
277 19 : return ref(cd.change().getDest().branch()).change(cd);
278 0 : } catch (StorageException e) {
279 0 : return FailedPermissionBackend.change("unavailable", e);
280 : }
281 : }
282 :
283 : /** Returns an instance scoped for the change, and its destination ref and project. */
284 : public ForChange change(ChangeNotes notes) {
285 38 : return ref(notes.getChange().getDest().branch()).change(notes);
286 : }
287 :
288 : /**
289 : * Verify scoped user can {@code perm}, throwing if denied.
290 : *
291 : * <p>Should be used in REST API handlers where the thrown {@link AuthException} can be
292 : * propagated. In business logic, where the exception would have to be caught, prefer using
293 : * {@link #test(CoreOrPluginProjectPermission)}.
294 : */
295 : public abstract void check(CoreOrPluginProjectPermission perm)
296 : throws AuthException, PermissionBackendException;
297 :
298 : /** Filter {@code permSet} to permissions scoped user might be able to perform. */
299 : public abstract <T extends CoreOrPluginProjectPermission> Set<T> test(Collection<T> permSet)
300 : throws PermissionBackendException;
301 :
302 : public boolean test(CoreOrPluginProjectPermission perm) throws PermissionBackendException {
303 143 : if (perm instanceof ProjectPermission) {
304 143 : return test(EnumSet.of((ProjectPermission) perm)).contains(perm);
305 : }
306 :
307 : // TODO(xchangcheng): implement for plugin defined project permissions.
308 0 : return false;
309 : }
310 :
311 : public boolean testOrFalse(CoreOrPluginProjectPermission perm) {
312 : try {
313 64 : return test(perm);
314 0 : } catch (PermissionBackendException e) {
315 0 : logger.atWarning().withCause(e).log("Cannot test %s; assuming false", perm);
316 0 : return false;
317 : }
318 : }
319 :
320 : public abstract BooleanCondition testCond(CoreOrPluginProjectPermission perm);
321 :
322 : /**
323 : * Filter a list of references by visibility.
324 : *
325 : * @param refs a collection of references to filter.
326 : * @param repo an open {@link Repository} handle for this instance's project
327 : * @param opts further options for filtering.
328 : * @return a partition of the provided refs that are visible to the user that this instance is
329 : * scoped to.
330 : * @throws PermissionBackendException if failure consulting backend configuration.
331 : */
332 : public abstract Collection<Ref> filter(
333 : Collection<Ref> refs, Repository repo, RefFilterOptions opts)
334 : throws PermissionBackendException;
335 : }
336 :
337 : /** Options for filtering refs using {@link ForProject}. */
338 : @AutoValue
339 135 : public abstract static class RefFilterOptions {
340 : /** Remove all NoteDb refs (refs/changes/*, refs/users/*, edit refs) from the result. */
341 : public abstract boolean filterMeta();
342 :
343 : /**
344 : * Select only refs with names matching prefixes per {@link
345 : * org.eclipse.jgit.lib.RefDatabase#getRefsByPrefix}.
346 : */
347 : public abstract ImmutableList<String> prefixes();
348 :
349 : public abstract Builder toBuilder();
350 :
351 : public static Builder builder() {
352 135 : return new AutoValue_PermissionBackend_RefFilterOptions.Builder()
353 135 : .setFilterMeta(false)
354 135 : .setPrefixes(Collections.singletonList(""));
355 : }
356 :
357 : @AutoValue.Builder
358 135 : public abstract static class Builder {
359 : public abstract Builder setFilterMeta(boolean val);
360 :
361 : public abstract Builder setPrefixes(List<String> prefixes);
362 :
363 : public abstract RefFilterOptions build();
364 : }
365 :
366 : public static RefFilterOptions defaults() {
367 135 : return builder().build();
368 : }
369 : }
370 :
371 : /** PermissionBackend scoped to a user, project and reference. */
372 132 : public abstract static class ForRef {
373 : /** Returns a fully qualified resource path that this instance is scoped to. */
374 : public abstract String resourcePath();
375 :
376 : /** Returns an instance scoped to change. */
377 : public abstract ForChange change(ChangeData cd);
378 :
379 : /** Returns an instance scoped to change. */
380 : public abstract ForChange change(ChangeNotes notes);
381 :
382 : /**
383 : * Verify scoped user can {@code perm}, throwing if denied.
384 : *
385 : * <p>Should be used in REST API handlers where the thrown {@link AuthException} can be
386 : * propagated. In business logic, where the exception would have to be caught, prefer using
387 : * {@link #test(RefPermission)}.
388 : */
389 : public abstract void check(RefPermission perm) throws AuthException, PermissionBackendException;
390 :
391 : /** Filter {@code permSet} to permissions scoped user might be able to perform. */
392 : public abstract Set<RefPermission> test(Collection<RefPermission> permSet)
393 : throws PermissionBackendException;
394 :
395 : public boolean test(RefPermission perm) throws PermissionBackendException {
396 129 : return test(EnumSet.of(perm)).contains(perm);
397 : }
398 :
399 : /**
400 : * Test if user may be able to perform the permission.
401 : *
402 : * <p>Similar to {@link #test(RefPermission)} except this method returns {@code false} instead
403 : * of throwing an exception.
404 : *
405 : * @param perm the permission to test.
406 : * @return true if the user might be able to perform the permission; false if the user may be
407 : * missing the necessary grants or state, or if the backend threw an exception.
408 : */
409 : public boolean testOrFalse(RefPermission perm) {
410 : try {
411 112 : return test(perm);
412 0 : } catch (PermissionBackendException e) {
413 0 : logger.atWarning().withCause(e).log("Cannot test %s; assuming false", perm);
414 0 : return false;
415 : }
416 : }
417 :
418 : public abstract BooleanCondition testCond(RefPermission perm);
419 : }
420 :
421 : /** PermissionBackend scoped to a user, project, reference and change. */
422 103 : public abstract static class ForChange {
423 : /** Returns the fully qualified resource path that this instance is scoped to. */
424 : public abstract String resourcePath();
425 :
426 : /**
427 : * Verify scoped user can {@code perm}, throwing if denied.
428 : *
429 : * <p>Should be used in REST API handlers where the thrown {@link AuthException} can be
430 : * propagated. In business logic, where the exception would have to be caught, prefer using
431 : * {@link #test(ChangePermissionOrLabel)}.
432 : */
433 : public abstract void check(ChangePermissionOrLabel perm)
434 : throws AuthException, PermissionBackendException;
435 :
436 : /** Filter {@code permSet} to permissions scoped user might be able to perform. */
437 : public abstract <T extends ChangePermissionOrLabel> Set<T> test(Collection<T> permSet)
438 : throws PermissionBackendException;
439 :
440 : public boolean test(ChangePermissionOrLabel perm) throws PermissionBackendException {
441 103 : return test(Collections.singleton(perm)).contains(perm);
442 : }
443 :
444 : /**
445 : * Test if user may be able to perform the permission.
446 : *
447 : * <p>Similar to {@link #test(ChangePermissionOrLabel)} except this method returns {@code false}
448 : * instead of throwing an exception.
449 : *
450 : * @param perm the permission to test.
451 : * @return true if the user might be able to perform the permission; false if the user may be
452 : * missing the necessary grants or state, or if the backend threw an exception.
453 : */
454 : public boolean testOrFalse(ChangePermissionOrLabel perm) {
455 : try {
456 57 : return test(perm);
457 0 : } catch (PermissionBackendException e) {
458 0 : logger.atWarning().withCause(e).log("Cannot test %s; assuming false", perm);
459 0 : return false;
460 : }
461 : }
462 :
463 : public abstract BooleanCondition testCond(ChangePermissionOrLabel perm);
464 :
465 : /**
466 : * Test which values of a label the user may be able to set.
467 : *
468 : * @param label definition of the label to test values of.
469 : * @return set containing values the user may be able to use; may be empty if none.
470 : * @throws PermissionBackendException if failure consulting backend configuration.
471 : */
472 : public Set<LabelPermission.WithValue> test(LabelType label) throws PermissionBackendException {
473 103 : return test(valuesOf(requireNonNull(label, "LabelType")));
474 : }
475 :
476 : /**
477 : * Test which values of a group of labels the user may be able to set.
478 : *
479 : * @param types definition of the labels to test values of.
480 : * @return set containing values the user may be able to use; may be empty if none.
481 : * @throws PermissionBackendException if failure consulting backend configuration.
482 : */
483 : public Set<LabelPermission.WithValue> testLabels(Collection<LabelType> types)
484 : throws PermissionBackendException {
485 0 : requireNonNull(types, "LabelType");
486 0 : return test(types.stream().flatMap(t -> valuesOf(t).stream()).collect(toSet()));
487 : }
488 :
489 : private static Set<LabelPermission.WithValue> valuesOf(LabelType label) {
490 103 : return label.getValues().stream()
491 103 : .map(v -> new LabelPermission.WithValue(label, v))
492 103 : .collect(toSet());
493 : }
494 : }
495 : }
|