Change capabilities collection to parse using PermissionBackend

Add GlobalOrPluginPermission interface to allow PluginPermission to
represent a capability contributed by a plugin.  This can now be
tested through PermissionBackend instead of using CapabilityControl.

Parsing a capability uses test() to determine if the permission exists
for the target user.  Using test here is more correct than check as
the code isn't exercising the permission, its merely testing if the
user might be able to exercise the permission.

Update the capabilities REST API view to use the new PluginPermission,
which allows combining everything into a single test invocation.

Change-Id: I23f44ae3398a496fcb13421344ae545ac68cce93
This commit is contained in:
Shawn Pearce
2017-02-20 13:02:50 -08:00
committed by David Pursehouse
parent eacabe9154
commit e9e1af205c
8 changed files with 198 additions and 46 deletions

View File

@@ -14,7 +14,6 @@
package com.google.gerrit.server.account;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ChildCollection;
@@ -22,7 +21,13 @@ import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountResource.Capability;
import com.google.gerrit.server.permissions.GlobalOrPluginPermission;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.PluginPermission;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@@ -30,15 +35,18 @@ import com.google.inject.Singleton;
@Singleton
class Capabilities implements ChildCollection<AccountResource, AccountResource.Capability> {
private final Provider<CurrentUser> self;
private final PermissionBackend permissionBackend;
private final DynamicMap<RestView<AccountResource.Capability>> views;
private final Provider<GetCapabilities> get;
@Inject
Capabilities(
Provider<CurrentUser> self,
PermissionBackend permissionBackend,
DynamicMap<RestView<AccountResource.Capability>> views,
Provider<GetCapabilities> get) {
this.self = self;
this.permissionBackend = permissionBackend;
this.views = views;
this.get = get;
}
@@ -50,20 +58,39 @@ class Capabilities implements ChildCollection<AccountResource, AccountResource.C
@Override
public Capability parse(AccountResource parent, IdString id)
throws ResourceNotFoundException, AuthException {
if (self.get() != parent.getUser() && !self.get().getCapabilities().canAdministrateServer()) {
throw new AuthException("restricted to administrator");
throws ResourceNotFoundException, AuthException, PermissionBackendException {
IdentifiedUser target = parent.getUser();
if (self.get() != target) {
permissionBackend.user(self).check(GlobalPermission.ADMINISTRATE_SERVER);
}
String name = id.get();
CapabilityControl cap = parent.getUser().getCapabilities();
if (cap.canPerform(name)
|| (cap.canAdministrateServer() && GlobalCapability.isCapability(name))) {
return new AccountResource.Capability(parent.getUser(), name);
GlobalOrPluginPermission perm = parse(id);
if (permissionBackend.user(target).test(perm)) {
return new AccountResource.Capability(target, perm.permissionName());
}
throw new ResourceNotFoundException(id);
}
private GlobalOrPluginPermission parse(IdString id) throws ResourceNotFoundException {
String name = id.get();
GlobalOrPluginPermission perm = GlobalPermission.byName(name);
if (perm != null) {
return perm;
}
int dash = name.lastIndexOf('-');
if (dash < 0) {
throw new ResourceNotFoundException(id);
}
String pluginName = name.substring(0, dash);
String capability = name.substring(dash + 1);
if (pluginName.isEmpty() || capability.isEmpty()) {
throw new ResourceNotFoundException(id);
}
return new PluginPermission(pluginName, capability);
}
@Override
public DynamicMap<RestView<Capability>> views() {
return views;

View File

@@ -26,8 +26,10 @@ 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.GlobalOrPluginPermission;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.PluginPermission;
import com.google.gerrit.server.project.ProjectCache;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -216,8 +218,17 @@ public class CapabilityControl {
}
/** Do not use unless inside DefaultPermissionBackend. */
public boolean doCanForDefaultPermissionBackend(GlobalPermission perm)
public boolean doCanForDefaultPermissionBackend(GlobalOrPluginPermission perm)
throws PermissionBackendException {
if (perm instanceof GlobalPermission) {
return can((GlobalPermission) perm);
} else if (perm instanceof PluginPermission) {
return canPerform(perm.permissionName());
}
throw new PermissionBackendException(perm + " unsupported");
}
private boolean can(GlobalPermission perm) throws PermissionBackendException {
switch (perm) {
case ADMINISTRATE_SERVER:
return canAdministrateServer();

View File

@@ -29,14 +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.GlobalOrPluginPermission;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.PluginPermission;
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.LinkedHashMap;
import java.util.Map;
@@ -77,11 +78,10 @@ class GetCapabilities implements RestReadView<AccountResource> {
}
Map<String, Object> have = new LinkedHashMap<>();
for (GlobalPermission p : testGlobalPermissions(perm)) {
for (GlobalOrPluginPermission p : perm.test(permissionsToTest())) {
have.put(p.permissionName(), true);
}
addRanges(have, rsrc);
addPluginCapabilities(have, rsrc);
addPriority(have, rsrc);
return OutputFormat.JSON
@@ -89,20 +89,23 @@ class GetCapabilities implements RestReadView<AccountResource> {
.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()) {
private Set<GlobalOrPluginPermission> permissionsToTest() {
Set<GlobalOrPluginPermission> toTest = new HashSet<>();
for (GlobalPermission p : GlobalPermission.values()) {
if (want(p.permissionName())) {
toTest.add(p);
}
}
for (String pluginName : pluginCapabilities.plugins()) {
for (String capability : pluginCapabilities.byPlugin(pluginName).keySet()) {
PluginPermission p = new PluginPermission(pluginName, capability);
if (want(p.permissionName())) {
toTest.add(p);
}
}
} else {
toTest = EnumSet.allOf(GlobalPermission.class);
}
return perm.test(toTest);
return toTest;
}
private boolean want(String name) {
@@ -118,18 +121,6 @@ class GetCapabilities implements RestReadView<AccountResource> {
}
}
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);
if (want(name) && cc.canPerform(name)) {
have.put(name, true);
}
}
}
}
private void addPriority(Map<String, Object> have, AccountResource rsrc) {
QueueProvider.QueueType queue = rsrc.getUser().getCapabilities().getQueueType();
if (queue != QueueProvider.QueueType.INTERACTIVE

View File

@@ -0,0 +1,24 @@
// 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;
/** A {@link GlobalPermission} or a {@link PluginPermission}. */
public interface GlobalOrPluginPermission {
/** @return name used in {@code project.config} permissions. */
public String permissionName();
/** @return readable identifier of this permission for exception message. */
public String describeForException();
}

View File

@@ -14,10 +14,13 @@
package com.google.gerrit.server.permissions;
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.GlobalCapability;
import java.util.Locale;
public enum GlobalPermission {
/** Global server permissions built into Gerrit. */
public enum GlobalPermission implements GlobalOrPluginPermission {
ACCESS_DATABASE(GlobalCapability.ACCESS_DATABASE),
ADMINISTRATE_SERVER(GlobalCapability.ADMINISTRATE_SERVER),
CREATE_ACCOUNT(GlobalCapability.CREATE_ACCOUNT),
@@ -37,6 +40,21 @@ public enum GlobalPermission {
VIEW_PLUGINS(GlobalCapability.VIEW_PLUGINS),
VIEW_QUEUE(GlobalCapability.VIEW_QUEUE);
private static final ImmutableMap<String, GlobalPermission> BY_NAME;
static {
ImmutableMap.Builder<String, GlobalPermission> m = ImmutableMap.builder();
for (GlobalPermission p : values()) {
m.put(p.permissionName(), p);
}
BY_NAME = m.build();
}
@Nullable
public static GlobalPermission byName(String name) {
return BY_NAME.get(name);
}
private final String name;
GlobalPermission(String name) {
@@ -44,10 +62,12 @@ public enum GlobalPermission {
}
/** @return name used in {@code project.config} permissions. */
@Override
public String permissionName() {
return name;
}
@Override
public String describeForException() {
return toString().toLowerCase(Locale.US).replace('_', ' ');
}

View File

@@ -134,18 +134,18 @@ public abstract class PermissionBackend {
}
/** Verify scoped user can {@code perm}, throwing if denied. */
public abstract void check(GlobalPermission perm)
public abstract void check(GlobalOrPluginPermission perm)
throws AuthException, PermissionBackendException;
/** Filter {@code permSet} to permissions scoped user might be able to perform. */
public abstract Set<GlobalPermission> test(Collection<GlobalPermission> permSet)
public abstract <T extends GlobalOrPluginPermission> Set<T> test(Collection<T> permSet)
throws PermissionBackendException;
public boolean test(GlobalPermission perm) throws PermissionBackendException {
return test(EnumSet.of(perm)).contains(perm);
public boolean test(GlobalOrPluginPermission perm) throws PermissionBackendException {
return test(Collections.singleton(perm)).contains(perm);
}
public boolean testOrFalse(GlobalPermission perm) {
public boolean testOrFalse(GlobalOrPluginPermission perm) {
try {
return test(perm);
} catch (PermissionBackendException e) {

View File

@@ -0,0 +1,67 @@
// 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 java.util.Objects;
/** A global capability type permission used by a plugin. */
public class PluginPermission implements GlobalOrPluginPermission {
private final String pluginName;
private final String capability;
public PluginPermission(String pluginName, String capability) {
this.pluginName = checkNotNull(pluginName, "pluginName");
this.capability = checkNotNull(capability, "capability");
}
public String pluginName() {
return pluginName;
}
public String capability() {
return capability;
}
@Override
public String permissionName() {
return pluginName + '-' + capability;
}
@Override
public String describeForException() {
return capability + " for plugin " + pluginName;
}
@Override
public int hashCode() {
return Objects.hash(pluginName, capability);
}
@Override
public boolean equals(Object other) {
if (other instanceof PluginPermission) {
PluginPermission b = (PluginPermission) other;
return pluginName.equals(b.pluginName) && capability.equals(b.capability);
}
return false;
}
@Override
public String toString() {
return "PluginPermission[plugin=" + pluginName + ", capability=" + capability + ']';
}
}

View File

@@ -16,11 +16,12 @@ package com.google.gerrit.server.project;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.collect.Sets;
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.GlobalOrPluginPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.inject.Inject;
@@ -65,17 +66,18 @@ class DefaultPermissionBackend extends PermissionBackend {
}
@Override
public void check(GlobalPermission perm) throws AuthException, PermissionBackendException {
public void check(GlobalOrPluginPermission perm)
throws AuthException, PermissionBackendException {
if (!can(perm)) {
throw new AuthException(perm.describeForException() + " not permitted");
}
}
@Override
public Set<GlobalPermission> test(Collection<GlobalPermission> permSet)
public <T extends GlobalOrPluginPermission> Set<T> test(Collection<T> permSet)
throws PermissionBackendException {
EnumSet<GlobalPermission> ok = EnumSet.noneOf(GlobalPermission.class);
for (GlobalPermission perm : permSet) {
Set<T> ok = newSet(permSet);
for (T perm : permSet) {
if (can(perm)) {
ok.add(perm);
}
@@ -83,8 +85,18 @@ class DefaultPermissionBackend extends PermissionBackend {
return ok;
}
private boolean can(GlobalPermission perm) throws PermissionBackendException {
private boolean can(GlobalOrPluginPermission perm) throws PermissionBackendException {
return user.getCapabilities().doCanForDefaultPermissionBackend(perm);
}
}
private static <T extends GlobalOrPluginPermission> Set<T> newSet(Collection<T> permSet) {
if (permSet instanceof EnumSet) {
@SuppressWarnings({"unchecked", "rawtypes"})
Set<T> s = ((EnumSet) permSet).clone();
s.clear();
return s;
}
return Sets.newHashSetWithExpectedSize(permSet.size());
}
}