LCOV - code coverage report
Current view: top level - server/permissions - PermissionBackend.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 50 76 65.8 %
Date: 2022-11-19 15:00:39 Functions: 30 33 90.9 %

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

Generated by: LCOV version 1.16+git.20220603.dfeb750