Test GlobalCapabilities with PermissionBackend

Add GlobalPermission enum mapping the simple boolean
GlobalCapabilities to be checked by PermissionBackend.
Rewrite GetCapabilities handler in terms of this API.

Change-Id: Ie06a4f6b18b7bdfabbaa58c6aa9becbc1d9b6136
This commit is contained in:
Shawn Pearce
2017-02-19 19:34:18 -08:00
committed by David Pursehouse
parent cd84f3bb83
commit 625049c020
6 changed files with 225 additions and 116 deletions

View File

@@ -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<String> NAMES_ALL;
private static final List<String> 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<String> getRangeNames() {
return Collections.unmodifiableList(Arrays.asList(RANGE_NAMES));
}
/** @return the valid range for the capability if it has one, otherwise null. */

View File

@@ -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");
}
}

View File

@@ -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<AccountResource> {
private Set<String> query;
private final PermissionBackend permissionBackend;
private final Provider<CurrentUser> self;
private final DynamicMap<CapabilityDefinition> pluginCapabilities;
@Inject
GetCapabilities(Provider<CurrentUser> self, DynamicMap<CapabilityDefinition> pluginCapabilities) {
GetCapabilities(
PermissionBackend permissionBackend,
Provider<CurrentUser> self,
DynamicMap<CapabilityDefinition> 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<String, Object> 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<Map<String, Object>>() {}.getType());
}
private Set<GlobalPermission> testGlobalPermissions(PermissionBackend.WithUser perm)
throws PermissionBackendException {
EnumSet<GlobalPermission> 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<String, Object> 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<String, Object> 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<AccountResource> {
}
}
}
}
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<String, Object> 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<Map.Entry<String, Object>> itr = have.entrySet().iterator();
while (itr.hasNext()) {
Map.Entry<String, Object> 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<Map<String, Object>>() {}.getType());
}
private boolean want(String name) {
return query == null || query.contains(name.toLowerCase());
}
private static class Range {

View File

@@ -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('_', ' ');
}
}

View File

@@ -36,10 +36,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Checks authorization to perform an action on project, ref, or change.
*
* <p>{@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.
*
* <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
@@ -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.
*
* <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>
@@ -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<GlobalPermission> test(Collection<GlobalPermission> 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. */

View File

@@ -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<GlobalPermission> test(Collection<GlobalPermission> permSet)
throws PermissionBackendException {
EnumSet<GlobalPermission> 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);
}
}
}