summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/server/permissions/PermissionBackend.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/gerrit/server/permissions/PermissionBackend.java')
-rw-r--r--java/com/google/gerrit/server/permissions/PermissionBackend.java510
1 files changed, 510 insertions, 0 deletions
diff --git a/java/com/google/gerrit/server/permissions/PermissionBackend.java b/java/com/google/gerrit/server/permissions/PermissionBackend.java
new file mode 100644
index 0000000000..fea42b5de6
--- /dev/null
+++ b/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -0,0 +1,510 @@
+// 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.permissions;
+
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.toSet;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission;
+import com.google.gerrit.extensions.conditions.BooleanCondition;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.ImplementedBy;
+import com.google.inject.Provider;
+import com.google.inject.util.Providers;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * Checks authorization to perform an action on a project, reference, or change.
+ *
+ * <p>{@code check} methods should be used during action handlers to verify the user is allowed to
+ * exercise the specified permission. For convenience in implementation {@code check} methods throw
+ * {@link AuthException} if the permission is denied.
+ *
+ * <p>{@code test} methods should be used when constructing replies to the client and the result
+ * object needs to include a true/false hint indicating the user's ability to exercise the
+ * permission. This is suitable for configuring UI button state, but should not be relied upon to
+ * guard handlers before making state changes.
+ *
+ * <p>{@code PermissionBackend} is a singleton for the server, acting as a factory for lightweight
+ * request instances. Implementation classes may cache supporting data inside of {@link WithUser},
+ * {@link ForProject}, {@link ForRef}, and {@link ForChange} instances, in addition to storing
+ * within {@link CurrentUser} using a {@link com.google.gerrit.server.CurrentUser.PropertyKey}.
+ * {@link GlobalPermission} caching for {@link WithUser} may best cached inside {@link CurrentUser}
+ * as {@link WithUser} instances are frequently created.
+ *
+ * <p>Example use:
+ *
+ * <pre>
+ * private final PermissionBackend permissions;
+ * private final Provider<CurrentUser> user;
+ *
+ * @Inject
+ * Foo(PermissionBackend permissions, Provider<CurrentUser> user) {
+ * this.permissions = permissions;
+ * this.user = user;
+ * }
+ *
+ * public void apply(...) {
+ * permissions.user(user).change(cd).check(ChangePermission.SUBMIT);
+ * }
+ *
+ * public UiAction.Description getDescription(ChangeResource rsrc) {
+ * return new UiAction.Description()
+ * .setLabel("Submit")
+ * .setVisible(rsrc.permissions().testCond(ChangePermission.SUBMIT));
+ * }
+ * </pre>
+ */
+@ImplementedBy(DefaultPermissionBackend.class)
+public abstract class PermissionBackend {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ /** Returns an instance scoped to the current user. */
+ public abstract WithUser currentUser();
+
+ /**
+ * Returns an instance scoped to the specified user. Should be used in cases where the user could
+ * either be the issuer of the current request or an impersonated user. PermissionBackends that do
+ * not support impersonation can fail with an {@code IllegalStateException}.
+ *
+ * <p>If an instance scoped to the current user is desired, use {@code currentUser()} instead.
+ */
+ public abstract WithUser user(CurrentUser user);
+
+ /**
+ * Returns an instance scoped to the provided user. Should be used in cases where the caller wants
+ * to check the permissions of a user who is not the issuer of the current request and not the
+ * target of impersonation.
+ *
+ * <p>Usage should be very limited as this can expose a group-oracle.
+ */
+ public abstract WithUser absentUser(Account.Id id);
+
+ /**
+ * Check whether this {@code PermissionBackend} respects the same global capabilities as the
+ * {@link DefaultPermissionBackend}.
+ *
+ * <p>If true, then it makes sense for downstream callers to refer to built-in Gerrit capability
+ * names in user-facing error messages, for example.
+ *
+ * @return whether this is the default permission backend.
+ */
+ public boolean usesDefaultCapabilities() {
+ return false;
+ }
+
+ /**
+ * Throw {@link ResourceNotFoundException} if this backend does not use the default global
+ * capabilities.
+ */
+ public void checkUsesDefaultCapabilities() throws ResourceNotFoundException {
+ if (!usesDefaultCapabilities()) {
+ throw new ResourceNotFoundException("Gerrit capabilities not used on this server");
+ }
+ }
+
+ /**
+ * Bulk evaluate a set of {@link PermissionBackendCondition} for view handling.
+ *
+ * <p>Overridden implementations should call {@link PermissionBackendCondition#set(boolean)} to
+ * cache the result of {@code testOrFalse} in the condition for later evaluation. Caching the
+ * result will bypass the usual invocation of {@code testOrFalse}.
+ *
+ * @param conds conditions to consider.
+ */
+ public void bulkEvaluateTest(Set<PermissionBackendCondition> conds) {
+ // Do nothing by default. The default implementation of PermissionBackendCondition
+ // delegates to the appropriate testOrFalse method in PermissionBackend.
+ }
+
+ /** PermissionBackend with an optional per-request ReviewDb handle. */
+ public abstract static class AcceptsReviewDb<T> {
+ protected Provider<ReviewDb> db;
+
+ public T database(Provider<ReviewDb> db) {
+ if (db != null) {
+ this.db = db;
+ }
+ return self();
+ }
+
+ public T database(ReviewDb db) {
+ return database(Providers.of(requireNonNull(db, "ReviewDb")));
+ }
+
+ @SuppressWarnings("unchecked")
+ private T self() {
+ return (T) this;
+ }
+ }
+
+ /** PermissionBackend scoped to a specific user. */
+ public abstract static class WithUser extends AcceptsReviewDb<WithUser> {
+ /** Returns an instance scoped for the specified project. */
+ public abstract ForProject project(Project.NameKey project);
+
+ /** Returns an instance scoped for the {@code ref}, and its parent project. */
+ public ForRef ref(Branch.NameKey ref) {
+ return project(ref.getParentKey()).ref(ref.get()).database(db);
+ }
+
+ /** Returns an instance scoped for the change, and its destination ref and project. */
+ public ForChange change(ChangeData cd) {
+ try {
+ return ref(cd.change().getDest()).change(cd);
+ } catch (OrmException e) {
+ return FailedPermissionBackend.change("unavailable", e);
+ }
+ }
+
+ /** Returns an instance scoped for the change, and its destination ref and project. */
+ public ForChange change(ChangeNotes notes) {
+ return ref(notes.getChange().getDest()).change(notes);
+ }
+
+ /**
+ * Returns an instance scoped for the change loaded from index, and its destination ref and
+ * project. This method should only be used when database access is harmful and potentially
+ * stale data from the index is acceptable.
+ */
+ public ForChange indexedChange(ChangeData cd, ChangeNotes notes) {
+ return ref(notes.getChange().getDest()).indexedChange(cd, notes);
+ }
+
+ /** Verify scoped user can {@code perm}, throwing if denied. */
+ public abstract void check(GlobalOrPluginPermission perm)
+ throws AuthException, PermissionBackendException;
+
+ /**
+ * Verify scoped user can perform at least one listed permission.
+ *
+ * <p>If {@code any} is empty, the method completes normally and allows the caller to continue.
+ * Since no permissions were supplied to check, its assumed no permissions are necessary to
+ * continue with the caller's operation.
+ *
+ * <p>If the user has at least one of the permissions in {@code any}, the method completes
+ * normally, possibly without checking all listed permissions.
+ *
+ * <p>If {@code any} is non-empty and the user has none, {@link AuthException} is thrown for one
+ * of the failed permissions.
+ *
+ * @param any set of permissions to check.
+ */
+ public void checkAny(Set<GlobalOrPluginPermission> any)
+ throws PermissionBackendException, AuthException {
+ for (Iterator<GlobalOrPluginPermission> itr = any.iterator(); itr.hasNext(); ) {
+ try {
+ check(itr.next());
+ return;
+ } catch (AuthException err) {
+ if (!itr.hasNext()) {
+ throw err;
+ }
+ }
+ }
+ }
+
+ /** Filter {@code permSet} to permissions scoped user might be able to perform. */
+ public abstract <T extends GlobalOrPluginPermission> Set<T> test(Collection<T> permSet)
+ throws PermissionBackendException;
+
+ public boolean test(GlobalOrPluginPermission perm) throws PermissionBackendException {
+ return test(Collections.singleton(perm)).contains(perm);
+ }
+
+ public boolean testOrFalse(GlobalOrPluginPermission perm) {
+ try {
+ return test(perm);
+ } catch (PermissionBackendException e) {
+ logger.atWarning().withCause(e).log("Cannot test %s; assuming false", perm);
+ return false;
+ }
+ }
+
+ public abstract BooleanCondition testCond(GlobalOrPluginPermission perm);
+
+ /**
+ * Filter a set of projects using {@code check(perm)}.
+ *
+ * @param perm required permission in a project to be included in result.
+ * @param projects candidate set of projects; may be empty.
+ * @return filtered set of {@code projects} where {@code check(perm)} was successful.
+ * @throws PermissionBackendException backend cannot access its internal state.
+ */
+ public Set<Project.NameKey> filter(ProjectPermission perm, Collection<Project.NameKey> projects)
+ throws PermissionBackendException {
+ requireNonNull(perm, "ProjectPermission");
+ requireNonNull(projects, "projects");
+ Set<Project.NameKey> allowed = Sets.newHashSetWithExpectedSize(projects.size());
+ for (Project.NameKey project : projects) {
+ try {
+ project(project).check(perm);
+ allowed.add(project);
+ } catch (AuthException e) {
+ // Do not include this project in allowed.
+ } catch (PermissionBackendException e) {
+ if (e.getCause() instanceof RepositoryNotFoundException) {
+ logger.atWarning().withCause(e).log(
+ "Could not find repository of the project %s", project.get());
+ // Do not include this project because doesn't exist
+ } else {
+ throw e;
+ }
+ }
+ }
+ return allowed;
+ }
+ }
+
+ /** PermissionBackend scoped to a user and project. */
+ public abstract static class ForProject extends AcceptsReviewDb<ForProject> {
+ /** Returns the fully qualified resource path that this instance is scoped to. */
+ public abstract String resourcePath();
+
+ /** Returns an instance scoped for {@code ref} in this project. */
+ public abstract ForRef ref(String ref);
+
+ /** Returns an instance scoped for the change, and its destination ref and project. */
+ public ForChange change(ChangeData cd) {
+ try {
+ return ref(cd.change().getDest().get()).change(cd);
+ } catch (OrmException e) {
+ return FailedPermissionBackend.change("unavailable", e);
+ }
+ }
+
+ /** Returns an instance scoped for the change, and its destination ref and project. */
+ public ForChange change(ChangeNotes notes) {
+ return ref(notes.getChange().getDest().get()).change(notes);
+ }
+
+ /**
+ * Returns an instance scoped for the change loaded from index, and its destination ref and
+ * project. This method should only be used when database access is harmful and potentially
+ * stale data from the index is acceptable.
+ */
+ public ForChange indexedChange(ChangeData cd, ChangeNotes notes) {
+ return ref(notes.getChange().getDest().get()).indexedChange(cd, notes);
+ }
+
+ /** Verify scoped user can {@code perm}, throwing if denied. */
+ public abstract void check(ProjectPermission perm)
+ throws AuthException, PermissionBackendException;
+
+ /** Filter {@code permSet} to permissions scoped user might be able to perform. */
+ public abstract Set<ProjectPermission> test(Collection<ProjectPermission> permSet)
+ throws PermissionBackendException;
+
+ public boolean test(ProjectPermission perm) throws PermissionBackendException {
+ return test(EnumSet.of(perm)).contains(perm);
+ }
+
+ public boolean testOrFalse(ProjectPermission perm) {
+ try {
+ return test(perm);
+ } catch (PermissionBackendException e) {
+ logger.atWarning().withCause(e).log("Cannot test %s; assuming false", perm);
+ return false;
+ }
+ }
+
+ public abstract BooleanCondition testCond(ProjectPermission perm);
+
+ /**
+ * Filter a map of references by visibility.
+ *
+ * @param refs a map of references to filter.
+ * @param repo an open {@link Repository} handle for this instance's project
+ * @param opts further options for filtering.
+ * @return a partition of the provided refs that are visible to the user that this instance is
+ * scoped to.
+ * @throws PermissionBackendException if failure consulting backend configuration.
+ */
+ public abstract Map<String, Ref> filter(
+ Map<String, Ref> refs, Repository repo, RefFilterOptions opts)
+ throws PermissionBackendException;
+ }
+
+ /** Options for filtering refs using {@link ForProject}. */
+ @AutoValue
+ public abstract static class RefFilterOptions {
+ /** Remove all NoteDb refs (refs/changes/*, refs/users/*, edit refs) from the result. */
+ public abstract boolean filterMeta();
+
+ /** Separately add reachable tags. */
+ public abstract boolean filterTagsSeparately();
+
+ public abstract Builder toBuilder();
+
+ public static Builder builder() {
+ return new AutoValue_PermissionBackend_RefFilterOptions.Builder()
+ .setFilterMeta(false)
+ .setFilterTagsSeparately(false);
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract Builder setFilterMeta(boolean val);
+
+ public abstract Builder setFilterTagsSeparately(boolean val);
+
+ public abstract RefFilterOptions build();
+ }
+
+ public static RefFilterOptions defaults() {
+ return builder().build();
+ }
+ }
+
+ /** PermissionBackend scoped to a user, project and reference. */
+ public abstract static class ForRef extends AcceptsReviewDb<ForRef> {
+ /** Returns a fully qualified resource path that this instance is scoped to. */
+ public abstract String resourcePath();
+
+ /** Returns an instance scoped to change. */
+ public abstract ForChange change(ChangeData cd);
+
+ /** Returns an instance scoped to change. */
+ public abstract ForChange change(ChangeNotes notes);
+
+ /**
+ * @return instance scoped to change loaded from index. This method should only be used when
+ * database access is harmful and potentially stale data from the index is acceptable.
+ */
+ public abstract ForChange indexedChange(ChangeData cd, ChangeNotes notes);
+
+ /** Verify scoped user can {@code perm}, throwing if denied. */
+ public abstract void check(RefPermission perm) throws AuthException, PermissionBackendException;
+
+ /** Filter {@code permSet} to permissions scoped user might be able to perform. */
+ public abstract Set<RefPermission> test(Collection<RefPermission> permSet)
+ throws PermissionBackendException;
+
+ public boolean test(RefPermission perm) throws PermissionBackendException {
+ return test(EnumSet.of(perm)).contains(perm);
+ }
+
+ /**
+ * Test if user may be able to perform the permission.
+ *
+ * <p>Similar to {@link #test(RefPermission)} except this method returns {@code false} instead
+ * of throwing an exception.
+ *
+ * @param perm the permission to test.
+ * @return true if the user might be able to perform the permission; false if the user may be
+ * missing the necessary grants or state, or if the backend threw an exception.
+ */
+ public boolean testOrFalse(RefPermission perm) {
+ try {
+ return test(perm);
+ } catch (PermissionBackendException e) {
+ logger.atWarning().withCause(e).log("Cannot test %s; assuming false", perm);
+ return false;
+ }
+ }
+
+ public abstract BooleanCondition testCond(RefPermission perm);
+ }
+
+ /** PermissionBackend scoped to a user, project, reference and change. */
+ public abstract static class ForChange extends AcceptsReviewDb<ForChange> {
+ /** Returns the fully qualified resource path that this instance is scoped to. */
+ public abstract String resourcePath();
+
+ /** Verify scoped user can {@code perm}, throwing if denied. */
+ public abstract void check(ChangePermissionOrLabel perm)
+ throws AuthException, PermissionBackendException;
+
+ /** Filter {@code permSet} to permissions scoped user might be able to perform. */
+ public abstract <T extends ChangePermissionOrLabel> Set<T> test(Collection<T> permSet)
+ throws PermissionBackendException;
+
+ public boolean test(ChangePermissionOrLabel perm) throws PermissionBackendException {
+ return test(Collections.singleton(perm)).contains(perm);
+ }
+
+ /**
+ * Test if user may be able to perform the permission.
+ *
+ * <p>Similar to {@link #test(ChangePermissionOrLabel)} except this method returns {@code false}
+ * instead of throwing an exception.
+ *
+ * @param perm the permission to test.
+ * @return true if the user might be able to perform the permission; false if the user may be
+ * missing the necessary grants or state, or if the backend threw an exception.
+ */
+ public boolean testOrFalse(ChangePermissionOrLabel perm) {
+ try {
+ return test(perm);
+ } catch (PermissionBackendException e) {
+ logger.atWarning().withCause(e).log("Cannot test %s; assuming false", perm);
+ return false;
+ }
+ }
+
+ public abstract BooleanCondition testCond(ChangePermissionOrLabel perm);
+
+ /**
+ * Test which values of a label the user may be able to set.
+ *
+ * @param label definition of the label to test values of.
+ * @return set containing values the user may be able to use; may be empty if none.
+ * @throws PermissionBackendException if failure consulting backend configuration.
+ */
+ public Set<LabelPermission.WithValue> test(LabelType label) throws PermissionBackendException {
+ return test(valuesOf(requireNonNull(label, "LabelType")));
+ }
+
+ /**
+ * Test which values of a group of labels the user may be able to set.
+ *
+ * @param types definition of the labels to test values of.
+ * @return set containing values the user may be able to use; may be empty if none.
+ * @throws PermissionBackendException if failure consulting backend configuration.
+ */
+ public Set<LabelPermission.WithValue> testLabels(Collection<LabelType> types)
+ throws PermissionBackendException {
+ requireNonNull(types, "LabelType");
+ return test(types.stream().flatMap((t) -> valuesOf(t).stream()).collect(toSet()));
+ }
+
+ private static Set<LabelPermission.WithValue> valuesOf(LabelType label) {
+ return label.getValues().stream()
+ .map((v) -> new LabelPermission.WithValue(label, v))
+ .collect(toSet());
+ }
+ }
+}