All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.permissions.PermissionBackend Maven / Gradle / Ivy

// 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 com.google.common.base.Preconditions.checkNotNull;
import static java.util.stream.Collectors.toSet;

import com.google.common.collect.Sets;
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.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.project.DefaultPermissionBackend;
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.Set;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Checks authorization to perform an action on a project, reference, or change.
 *
 * 

{@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. * *

{@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. * *

{@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. * *

Example use: * *

 *   private final PermissionBackend permissions;
 *   private final Provider user;
 *
 *   @Inject
 *   Foo(PermissionBackend permissions, Provider 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));
 * }
 * 
*/ @ImplementedBy(DefaultPermissionBackend.class) public abstract class PermissionBackend { private static final Logger logger = LoggerFactory.getLogger(PermissionBackend.class); /** @return lightweight factory scoped to answer for the specified user. */ public abstract WithUser user(CurrentUser user); /** @return lightweight factory scoped to answer for the specified user. */ public WithUser user(Provider user) { return user(checkNotNull(user, "Provider").get()); } /** * Bulk evaluate a collection of {@link PermissionBackendCondition} for view handling. * *

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}. * *

{@code conds} may contain duplicate entries (such as same user, resource, permission * triplet). When duplicates exist, implementations should set a result into all instances to * ensure {@code testOrFalse} does not get invoked during evaluation of the containing condition. * * @param conds conditions to consider. */ public void bulkEvaluateTest(Collection 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 { protected Provider db; public T database(Provider db) { if (db != null) { this.db = db; } return self(); } public T database(ReviewDb db) { return database(Providers.of(checkNotNull(db, "ReviewDb"))); } @SuppressWarnings("unchecked") private T self() { return (T) this; } } /** PermissionBackend scoped to a specific user. */ public abstract static class WithUser extends AcceptsReviewDb { /** @return instance scoped for the specified project. */ public abstract ForProject project(Project.NameKey project); /** @return 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); } /** @return 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); } } /** @return instance scoped for the change, and its destination ref and project. */ public ForChange change(ChangeNotes notes) { return ref(notes.getChange().getDest()).change(notes); } /** * @return 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. * *

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. * *

If the user has at least one of the permissions in {@code any}, the method completes * normally, possibly without checking all listed permissions. * *

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 any) throws PermissionBackendException, AuthException { for (Iterator 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 Set test(Collection 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.warn("Cannot test " + perm + "; assuming false", e); return false; } } public BooleanCondition testCond(GlobalOrPluginPermission perm) { return new PermissionBackendCondition.WithUser(this, 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 filter(ProjectPermission perm, Collection projects) throws PermissionBackendException { checkNotNull(perm, "ProjectPermission"); checkNotNull(projects, "projects"); Set 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.warn("Could not find repository of the project {} : ", project.get(), e); // 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 { /** @return new instance rescoped to same project, but different {@code user}. */ public abstract ForProject user(CurrentUser user); /** @return instance scoped for {@code ref} in this project. */ public abstract ForRef ref(String ref); /** @return 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); } } /** @return instance scoped for the change, and its destination ref and project. */ public ForChange change(ChangeNotes notes) { return ref(notes.getChange().getDest().get()).change(notes); } /** * @return 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 test(Collection 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.warn("Cannot test " + perm + "; assuming false", e); return false; } } public BooleanCondition testCond(ProjectPermission perm) { return new PermissionBackendCondition.ForProject(this, perm); } } /** PermissionBackend scoped to a user, project and reference. */ public abstract static class ForRef extends AcceptsReviewDb { /** @return new instance rescoped to same reference, but different {@code user}. */ public abstract ForRef user(CurrentUser user); /** @return instance scoped to change. */ public abstract ForChange change(ChangeData cd); /** @return 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 test(Collection 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. * *

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.warn("Cannot test " + perm + "; assuming false", e); return false; } } public BooleanCondition testCond(RefPermission perm) { return new PermissionBackendCondition.ForRef(this, perm); } } /** PermissionBackend scoped to a user, project, reference and change. */ public abstract static class ForChange extends AcceptsReviewDb { /** @return user this instance is scoped to. */ public abstract CurrentUser user(); /** @return new instance rescoped to same change, but different {@code user}. */ public abstract ForChange user(CurrentUser user); /** 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 Set test(Collection 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. * *

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.warn("Cannot test " + perm + "; assuming false", e); return false; } } public BooleanCondition testCond(ChangePermissionOrLabel perm) { return new PermissionBackendCondition.ForChange(this, 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 test(LabelType label) throws PermissionBackendException { return test(valuesOf(checkNotNull(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 testLabels(Collection types) throws PermissionBackendException { checkNotNull(types, "LabelType"); return test(types.stream().flatMap((t) -> valuesOf(t).stream()).collect(toSet())); } private static Set valuesOf(LabelType label) { return label.getValues().stream() .map((v) -> new LabelPermission.WithValue(label, v)) .collect(toSet()); } } }