diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java index 4c9b64a30d..6fd0e778ec 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java @@ -15,6 +15,7 @@ package com.google.gerrit.common.data; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -115,6 +116,9 @@ public class GlobalCapability { private static final List NAMES_ALL; private static final List NAMES_LC; + private static final String[] RANGE_NAMES = { + QUERY_LIMIT, BATCH_CHANGES_LIMIT, + }; static { NAMES_ALL = new ArrayList<>(); @@ -158,7 +162,16 @@ public class GlobalCapability { /** @return true if the capability should have a range attached. */ public static boolean hasRange(String varName) { - return QUERY_LIMIT.equalsIgnoreCase(varName) || BATCH_CHANGES_LIMIT.equalsIgnoreCase(varName); + for (String n : RANGE_NAMES) { + if (n.equalsIgnoreCase(varName)) { + return true; + } + } + return false; + } + + public static List getRangeNames() { + return Collections.unmodifiableList(Arrays.asList(RANGE_NAMES)); } /** @return the valid range for the capability if it has one, otherwise null. */ diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java index 12dee312e7..11630941aa 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java @@ -26,6 +26,8 @@ import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.PeerDaemonUser; import com.google.gerrit.server.git.QueueProvider; import com.google.gerrit.server.group.SystemGroupBackend; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.ProjectCache; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; @@ -75,21 +77,6 @@ public class CapabilityControl { return canAdministrateServer; } - /** @return true if the user can create an account for another user. */ - public boolean canCreateAccount() { - return canPerform(GlobalCapability.CREATE_ACCOUNT) || canAdministrateServer(); - } - - /** @return true if the user can create a group. */ - public boolean canCreateGroup() { - return canPerform(GlobalCapability.CREATE_GROUP) || canAdministrateServer(); - } - - /** @return true if the user can create a project. */ - public boolean canCreateProject() { - return canPerform(GlobalCapability.CREATE_PROJECT) || canAdministrateServer(); - } - /** @return true if the user can email reviewers. */ public boolean canEmailReviewers() { if (canEmailReviewers == null) { @@ -100,11 +87,6 @@ public class CapabilityControl { return canEmailReviewers; } - /** @return true if the user can kill any running task. */ - public boolean canKillTask() { - return canPerform(GlobalCapability.KILL_TASK) || canMaintainServer(); - } - /** @return true if the user can modify an account for another user. */ public boolean canModifyAccount() { return canPerform(GlobalCapability.MODIFY_ACCOUNT) || canAdministrateServer(); @@ -120,26 +102,11 @@ public class CapabilityControl { return canPerform(GlobalCapability.VIEW_CACHES) || canMaintainServer(); } - /** @return true if the user can flush the server's caches. */ - public boolean canFlushCaches() { - return canPerform(GlobalCapability.FLUSH_CACHES) || canMaintainServer(); - } - /** @return true if the user can perform basic server maintenance. */ public boolean canMaintainServer() { return canPerform(GlobalCapability.MAINTAIN_SERVER) || canAdministrateServer(); } - /** @return true if the user can view open connections. */ - public boolean canViewConnections() { - return canPerform(GlobalCapability.VIEW_CONNECTIONS) || canAdministrateServer(); - } - - /** @return true if the user can view the installed plugins. */ - public boolean canViewPlugins() { - return canPerform(GlobalCapability.VIEW_PLUGINS) || canAdministrateServer(); - } - /** @return true if the user can view the entire queue. */ public boolean canViewQueue() { return canPerform(GlobalCapability.VIEW_QUEUE) || canMaintainServer(); @@ -150,16 +117,6 @@ public class CapabilityControl { return canPerform(GlobalCapability.ACCESS_DATABASE); } - /** @return true if the user can stream Gerrit events. */ - public boolean canStreamEvents() { - return canPerform(GlobalCapability.STREAM_EVENTS) || canAdministrateServer(); - } - - /** @return true if the user can run the Git garbage collection. */ - public boolean canRunGC() { - return canPerform(GlobalCapability.RUN_GC) || canMaintainServer(); - } - /** @return true if the user can impersonate another user. */ public boolean canRunAs() { return canPerform(GlobalCapability.RUN_AS); @@ -278,4 +235,43 @@ public class CapabilityControl { private static boolean match(GroupMembership groups, PermissionRule rule) { return groups.contains(rule.getGroup().getUUID()); } + + /** Do not use unless inside DefaultPermissionBackend. */ + public boolean doCanForDefaultPermissionBackend(GlobalPermission perm) + throws PermissionBackendException { + switch (perm) { + case ACCESS_DATABASE: + return canAccessDatabase(); + case ADMINISTRATE_SERVER: + return canAdministrateServer(); + case EMAIL_REVIEWERS: + return canEmailReviewers(); + case MAINTAIN_SERVER: + return canMaintainServer(); + case MODIFY_ACCOUNT: + return canModifyAccount(); + case RUN_AS: + return canRunAs(); + case VIEW_ALL_ACCOUNTS: + return canViewAllAccounts(); + case VIEW_CACHES: + return canViewCaches(); + case VIEW_QUEUE: + return canViewQueue(); + + case FLUSH_CACHES: + case KILL_TASK: + case RUN_GC: + return canPerform(perm.permissionName()) || canMaintainServer(); + + case CREATE_ACCOUNT: + case CREATE_GROUP: + case CREATE_PROJECT: + case STREAM_EVENTS: + case VIEW_CONNECTIONS: + case VIEW_PLUGINS: + return canPerform(perm.permissionName()) || canAdministrateServer(); + } + throw new PermissionBackendException(perm + " unsupported"); + } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java index 876807da6a..1268ef2021 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java @@ -14,23 +14,7 @@ package com.google.gerrit.server.account; -import static com.google.gerrit.common.data.GlobalCapability.ACCESS_DATABASE; -import static com.google.gerrit.common.data.GlobalCapability.CREATE_ACCOUNT; -import static com.google.gerrit.common.data.GlobalCapability.CREATE_GROUP; -import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT; -import static com.google.gerrit.common.data.GlobalCapability.EMAIL_REVIEWERS; -import static com.google.gerrit.common.data.GlobalCapability.FLUSH_CACHES; -import static com.google.gerrit.common.data.GlobalCapability.KILL_TASK; -import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER; -import static com.google.gerrit.common.data.GlobalCapability.MODIFY_ACCOUNT; import static com.google.gerrit.common.data.GlobalCapability.PRIORITY; -import static com.google.gerrit.common.data.GlobalCapability.RUN_GC; -import static com.google.gerrit.common.data.GlobalCapability.STREAM_EVENTS; -import static com.google.gerrit.common.data.GlobalCapability.VIEW_ALL_ACCOUNTS; -import static com.google.gerrit.common.data.GlobalCapability.VIEW_CACHES; -import static com.google.gerrit.common.data.GlobalCapability.VIEW_CONNECTIONS; -import static com.google.gerrit.common.data.GlobalCapability.VIEW_PLUGINS; -import static com.google.gerrit.common.data.GlobalCapability.VIEW_QUEUE; import com.google.common.collect.Iterables; import com.google.gerrit.common.data.GlobalCapability; @@ -45,12 +29,15 @@ import com.google.gerrit.server.OptionUtil; import com.google.gerrit.server.OutputFormat; import com.google.gerrit.server.account.AccountResource.Capability; import com.google.gerrit.server.git.QueueProvider; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gson.reflect.TypeToken; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; +import java.util.EnumSet; import java.util.HashSet; -import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; @@ -67,34 +54,72 @@ class GetCapabilities implements RestReadView { private Set query; + private final PermissionBackend permissionBackend; private final Provider self; private final DynamicMap pluginCapabilities; @Inject - GetCapabilities(Provider self, DynamicMap pluginCapabilities) { + GetCapabilities( + PermissionBackend permissionBackend, + Provider self, + DynamicMap pluginCapabilities) { + this.permissionBackend = permissionBackend; this.self = self; this.pluginCapabilities = pluginCapabilities; } @Override - public Object apply(AccountResource resource) throws AuthException { - if (self.get() != resource.getUser() && !self.get().getCapabilities().canAdministrateServer()) { - throw new AuthException("restricted to administrator"); + public Object apply(AccountResource rsrc) throws AuthException, PermissionBackendException { + PermissionBackend.WithUser perm = permissionBackend.user(self); + if (self.get() != rsrc.getUser()) { + perm.check(GlobalPermission.ADMINISTRATE_SERVER); + perm = permissionBackend.user(rsrc.getUser()); } - CapabilityControl cc = resource.getUser().getCapabilities(); Map have = new LinkedHashMap<>(); - for (String name : GlobalCapability.getAllNames()) { - if (want(name)) { - if (GlobalCapability.hasRange(name)) { - if (cc.hasExplicitRange(name)) { - have.put(name, new Range(cc.getRange(name))); - } - } else if (!name.equals(PRIORITY) && cc.canPerform(name)) { - have.put(name, true); + for (GlobalPermission p : testGlobalPermissions(perm)) { + have.put(p.permissionName(), true); + } + addRanges(have, rsrc); + addPluginCapabilities(have, rsrc); + addPriority(have, rsrc); + + return OutputFormat.JSON + .newGson() + .toJsonTree(have, new TypeToken>() {}.getType()); + } + + private Set testGlobalPermissions(PermissionBackend.WithUser perm) + throws PermissionBackendException { + EnumSet toTest; + if (query != null) { + toTest = EnumSet.noneOf(GlobalPermission.class); + for (GlobalPermission p : GlobalPermission.values()) { + if (want(p.permissionName())) { + toTest.add(p); } } + } else { + toTest = EnumSet.allOf(GlobalPermission.class); } + return perm.test(toTest); + } + + private boolean want(String name) { + return query == null || query.contains(name.toLowerCase()); + } + + private void addRanges(Map have, AccountResource rsrc) { + CapabilityControl cc = rsrc.getUser().getCapabilities(); + for (String name : GlobalCapability.getRangeNames()) { + if (want(name) && cc.hasExplicitRange(name)) { + have.put(name, new Range(cc.getRange(name))); + } + } + } + + private void addPluginCapabilities(Map have, AccountResource rsrc) { + CapabilityControl cc = rsrc.getUser().getCapabilities(); for (String pluginName : pluginCapabilities.plugins()) { for (String capability : pluginCapabilities.byPlugin(pluginName).keySet()) { String name = String.format("%s-%s", pluginName, capability); @@ -103,47 +128,14 @@ class GetCapabilities implements RestReadView { } } } + } - have.put(ACCESS_DATABASE, cc.canAccessDatabase()); - have.put(CREATE_ACCOUNT, cc.canCreateAccount()); - have.put(CREATE_GROUP, cc.canCreateGroup()); - have.put(CREATE_PROJECT, cc.canCreateProject()); - have.put(EMAIL_REVIEWERS, cc.canEmailReviewers()); - have.put(FLUSH_CACHES, cc.canFlushCaches()); - have.put(KILL_TASK, cc.canKillTask()); - have.put(MAINTAIN_SERVER, cc.canMaintainServer()); - have.put(MODIFY_ACCOUNT, cc.canModifyAccount()); - have.put(RUN_GC, cc.canRunGC()); - have.put(STREAM_EVENTS, cc.canStreamEvents()); - have.put(VIEW_ALL_ACCOUNTS, cc.canViewAllAccounts()); - have.put(VIEW_CACHES, cc.canViewCaches()); - have.put(VIEW_CONNECTIONS, cc.canViewConnections()); - have.put(VIEW_PLUGINS, cc.canViewPlugins()); - have.put(VIEW_QUEUE, cc.canViewQueue()); - - QueueProvider.QueueType queue = cc.getQueueType(); + private void addPriority(Map have, AccountResource rsrc) { + QueueProvider.QueueType queue = rsrc.getUser().getCapabilities().getQueueType(); if (queue != QueueProvider.QueueType.INTERACTIVE || (query != null && query.contains(PRIORITY))) { have.put(PRIORITY, queue); } - - Iterator> itr = have.entrySet().iterator(); - while (itr.hasNext()) { - Map.Entry e = itr.next(); - if (!want(e.getKey())) { - itr.remove(); - } else if (e.getValue() instanceof Boolean && !((Boolean) e.getValue())) { - itr.remove(); - } - } - - return OutputFormat.JSON - .newGson() - .toJsonTree(have, new TypeToken>() {}.getType()); - } - - private boolean want(String name) { - return query == null || query.contains(name.toLowerCase()); } private static class Range { diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/GlobalPermission.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/GlobalPermission.java new file mode 100644 index 0000000000..575a08bb96 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/GlobalPermission.java @@ -0,0 +1,54 @@ +// 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 com.google.gerrit.common.data.GlobalCapability; +import java.util.Locale; + +public enum GlobalPermission { + ACCESS_DATABASE(GlobalCapability.ACCESS_DATABASE), + ADMINISTRATE_SERVER(GlobalCapability.ADMINISTRATE_SERVER), + CREATE_ACCOUNT(GlobalCapability.CREATE_ACCOUNT), + CREATE_GROUP(GlobalCapability.CREATE_GROUP), + CREATE_PROJECT(GlobalCapability.CREATE_PROJECT), + EMAIL_REVIEWERS(GlobalCapability.EMAIL_REVIEWERS), + FLUSH_CACHES(GlobalCapability.FLUSH_CACHES), + KILL_TASK(GlobalCapability.KILL_TASK), + MAINTAIN_SERVER(GlobalCapability.MAINTAIN_SERVER), + MODIFY_ACCOUNT(GlobalCapability.MODIFY_ACCOUNT), + RUN_AS(GlobalCapability.RUN_AS), + RUN_GC(GlobalCapability.RUN_GC), + STREAM_EVENTS(GlobalCapability.STREAM_EVENTS), + VIEW_ALL_ACCOUNTS(GlobalCapability.VIEW_ALL_ACCOUNTS), + VIEW_CACHES(GlobalCapability.VIEW_CACHES), + VIEW_CONNECTIONS(GlobalCapability.VIEW_CONNECTIONS), + VIEW_PLUGINS(GlobalCapability.VIEW_PLUGINS), + VIEW_QUEUE(GlobalCapability.VIEW_QUEUE); + + private final String name; + + GlobalPermission(String name) { + this.name = name; + } + + /** @return name used in {@code project.config} permissions. */ + public String permissionName() { + return name; + } + + public String describeForException() { + return toString().toLowerCase(Locale.US).replace('_', ' '); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java index 7a7694986c..b3a858c62a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java @@ -36,10 +36,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * Checks authorization to perform an action on project, ref, or change. - * - *

{@code PermissionBackend} should be a singleton for the server, acting as a factory for - * lightweight request instances. + * 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 @@ -50,6 +47,13 @@ import org.slf4j.LoggerFactory; * 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: * *

@@ -128,6 +132,27 @@ public abstract class PermissionBackend {
     public ForChange change(ChangeNotes notes) {
       return ref(notes.getChange().getDest()).change(notes);
     }
+
+    /** Verify scoped user can {@code perm}, throwing if denied. */
+    public abstract void check(GlobalPermission 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(GlobalPermission perm) throws PermissionBackendException {
+      return test(EnumSet.of(perm)).contains(perm);
+    }
+
+    public boolean testOrFalse(GlobalPermission perm) {
+      try {
+        return test(perm);
+      } catch (PermissionBackendException e) {
+        logger.warn("Cannot test " + perm + "; assuming false", e);
+        return false;
+      }
+    }
   }
 
   /** PermissionBackend scoped to a user and project. */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java
index 51fe493e4d..a8f9efa223 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java
@@ -16,13 +16,19 @@ package com.google.gerrit.server.project;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.FailedPermissionBackend;
+import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.Set;
 
 @Singleton
 class DefaultPermissionBackend extends PermissionBackend {
@@ -57,5 +63,28 @@ class DefaultPermissionBackend extends PermissionBackend {
         return FailedPermissionBackend.project("unavailable", e);
       }
     }
+
+    @Override
+    public void check(GlobalPermission perm) throws AuthException, PermissionBackendException {
+      if (!can(perm)) {
+        throw new AuthException(perm.describeForException() + " not permitted");
+      }
+    }
+
+    @Override
+    public Set test(Collection permSet)
+        throws PermissionBackendException {
+      EnumSet ok = EnumSet.noneOf(GlobalPermission.class);
+      for (GlobalPermission perm : permSet) {
+        if (can(perm)) {
+          ok.add(perm);
+        }
+      }
+      return ok;
+    }
+
+    private boolean can(GlobalPermission perm) throws PermissionBackendException {
+      return user.getCapabilities().doCanForDefaultPermissionBackend(perm);
+    }
   }
 }