Convert RequireCapability checks to PermissionBackend
Replace CapabilityUtils with support in PermissionBackend to check if the caller has at least one of the specified permissions parsed from class annotation. This enables hiding canPerform(String) from CapabilityControl, which makes it much harder to bypass the PermissionBackend. Assume anyone with ADMINISTRATE_SERVER also has any PluginPermission. This is carried over from CapabilityUtils, which skip any further checks when the user has canAdministrateServer. Update the error message in GarbageCollectionIT to now be the generic "maintain server not permitted". Change-Id: I9458bd55fa1c9709557ae1ad95a57a1d968c52a3
This commit is contained in:
committed by
David Pursehouse
parent
e9e1af205c
commit
79a899e505
@@ -142,11 +142,8 @@ public class CapabilityControl {
|
||||
return QueueProvider.QueueType.INTERACTIVE;
|
||||
}
|
||||
|
||||
/** True if the user has this permission. Works only for non labels. */
|
||||
public boolean canPerform(String permissionName) {
|
||||
if (GlobalCapability.ADMINISTRATE_SERVER.equals(permissionName)) {
|
||||
return canAdministrateServer();
|
||||
}
|
||||
/** @return true if the user has this permission. */
|
||||
private boolean canPerform(String permissionName) {
|
||||
return !access(permissionName).isEmpty();
|
||||
}
|
||||
|
||||
@@ -223,7 +220,7 @@ public class CapabilityControl {
|
||||
if (perm instanceof GlobalPermission) {
|
||||
return can((GlobalPermission) perm);
|
||||
} else if (perm instanceof PluginPermission) {
|
||||
return canPerform(perm.permissionName());
|
||||
return canPerform(perm.permissionName()) || canAdministrateServer();
|
||||
}
|
||||
throw new PermissionBackendException(perm + " unsupported");
|
||||
}
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
// Copyright (C) 2013 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.account;
|
||||
|
||||
import com.google.gerrit.extensions.annotations.CapabilityScope;
|
||||
import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
|
||||
import com.google.gerrit.extensions.annotations.RequiresCapability;
|
||||
import com.google.gerrit.extensions.restapi.AuthException;
|
||||
import com.google.gerrit.server.CurrentUser;
|
||||
import com.google.inject.Provider;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.Arrays;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class CapabilityUtils {
|
||||
private static final Logger log = LoggerFactory.getLogger(CapabilityUtils.class);
|
||||
|
||||
public static void checkRequiresCapability(
|
||||
Provider<CurrentUser> userProvider, String pluginName, Class<?> clazz) throws AuthException {
|
||||
checkRequiresCapability(userProvider.get(), pluginName, clazz);
|
||||
}
|
||||
|
||||
public static void checkRequiresCapability(CurrentUser user, String pluginName, Class<?> clazz)
|
||||
throws AuthException {
|
||||
RequiresCapability rc = getClassAnnotation(clazz, RequiresCapability.class);
|
||||
RequiresAnyCapability rac = getClassAnnotation(clazz, RequiresAnyCapability.class);
|
||||
if (rc != null && rac != null) {
|
||||
log.error(
|
||||
String.format(
|
||||
"Class %s uses both @%s and @%s",
|
||||
clazz.getName(),
|
||||
RequiresCapability.class.getSimpleName(),
|
||||
RequiresAnyCapability.class.getSimpleName()));
|
||||
throw new AuthException("cannot check capability");
|
||||
}
|
||||
CapabilityControl ctl = user.getCapabilities();
|
||||
if (ctl.canAdministrateServer()) {
|
||||
return;
|
||||
}
|
||||
checkRequiresCapability(ctl, pluginName, clazz, rc);
|
||||
checkRequiresAnyCapability(ctl, pluginName, clazz, rac);
|
||||
}
|
||||
|
||||
private static void checkRequiresCapability(
|
||||
CapabilityControl ctl, String pluginName, Class<?> clazz, RequiresCapability rc)
|
||||
throws AuthException {
|
||||
if (rc == null) {
|
||||
return;
|
||||
}
|
||||
String capability = resolveCapability(pluginName, rc.value(), rc.scope(), clazz);
|
||||
if (!ctl.canPerform(capability)) {
|
||||
throw new AuthException(
|
||||
String.format("Capability %s is required to access this resource", capability));
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkRequiresAnyCapability(
|
||||
CapabilityControl ctl, String pluginName, Class<?> clazz, RequiresAnyCapability rac)
|
||||
throws AuthException {
|
||||
if (rac == null) {
|
||||
return;
|
||||
}
|
||||
if (rac.value().length == 0) {
|
||||
log.error(
|
||||
String.format(
|
||||
"Class %s uses @%s with no capabilities listed",
|
||||
clazz.getName(), RequiresAnyCapability.class.getSimpleName()));
|
||||
throw new AuthException("cannot check capability");
|
||||
}
|
||||
for (String capability : rac.value()) {
|
||||
capability = resolveCapability(pluginName, capability, rac.scope(), clazz);
|
||||
if (ctl.canPerform(capability)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new AuthException(
|
||||
"One of the following capabilities is required to access this"
|
||||
+ " resource: "
|
||||
+ Arrays.asList(rac.value()));
|
||||
}
|
||||
|
||||
private static String resolveCapability(
|
||||
String pluginName, String capability, CapabilityScope scope, Class<?> clazz)
|
||||
throws AuthException {
|
||||
if (pluginName != null
|
||||
&& !"gerrit".equals(pluginName)
|
||||
&& (scope == CapabilityScope.PLUGIN || scope == CapabilityScope.CONTEXT)) {
|
||||
capability = String.format("%s-%s", pluginName, capability);
|
||||
} else if (scope == CapabilityScope.PLUGIN) {
|
||||
log.error(
|
||||
String.format(
|
||||
"Class %s uses @%s(scope=%s), but is not within a plugin",
|
||||
clazz.getName(),
|
||||
RequiresCapability.class.getSimpleName(),
|
||||
CapabilityScope.PLUGIN.name()));
|
||||
throw new AuthException("cannot check capability");
|
||||
}
|
||||
return capability;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an instance of the specified annotation, walking up the inheritance tree if necessary.
|
||||
*
|
||||
* @param <T> Annotation type to search for
|
||||
* @param clazz root class to search, may be null
|
||||
* @param annotationClass class object of Annotation subclass to search for
|
||||
* @return the requested annotation or null if none
|
||||
*/
|
||||
private static <T extends Annotation> T getClassAnnotation(
|
||||
Class<?> clazz, Class<T> annotationClass) {
|
||||
for (; clazz != null; clazz = clazz.getSuperclass()) {
|
||||
T t = clazz.getAnnotation(annotationClass);
|
||||
if (t != null) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@
|
||||
package com.google.gerrit.server.api.accounts;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.gerrit.server.account.CapabilityUtils.checkRequiresCapability;
|
||||
|
||||
import com.google.gerrit.extensions.api.accounts.AccountApi;
|
||||
import com.google.gerrit.extensions.api.accounts.AccountInput;
|
||||
@@ -32,6 +31,9 @@ import com.google.gerrit.server.account.AccountResource;
|
||||
import com.google.gerrit.server.account.AccountsCollection;
|
||||
import com.google.gerrit.server.account.CreateAccount;
|
||||
import com.google.gerrit.server.account.QueryAccounts;
|
||||
import com.google.gerrit.server.permissions.GlobalPermission;
|
||||
import com.google.gerrit.server.permissions.PermissionBackend;
|
||||
import com.google.gerrit.server.permissions.PermissionBackendException;
|
||||
import com.google.gwtorm.server.OrmException;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
@@ -44,6 +46,7 @@ import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||
public class AccountsImpl implements Accounts {
|
||||
private final AccountsCollection accounts;
|
||||
private final AccountApiImpl.Factory api;
|
||||
private final PermissionBackend permissionBackend;
|
||||
private final Provider<CurrentUser> self;
|
||||
private final CreateAccount.Factory createAccount;
|
||||
private final Provider<QueryAccounts> queryAccountsProvider;
|
||||
@@ -52,11 +55,13 @@ public class AccountsImpl implements Accounts {
|
||||
AccountsImpl(
|
||||
AccountsCollection accounts,
|
||||
AccountApiImpl.Factory api,
|
||||
PermissionBackend permissionBackend,
|
||||
Provider<CurrentUser> self,
|
||||
CreateAccount.Factory createAccount,
|
||||
Provider<QueryAccounts> queryAccountsProvider) {
|
||||
this.accounts = accounts;
|
||||
this.api = api;
|
||||
this.permissionBackend = permissionBackend;
|
||||
this.self = self;
|
||||
this.createAccount = createAccount;
|
||||
this.queryAccountsProvider = queryAccountsProvider;
|
||||
@@ -96,12 +101,12 @@ public class AccountsImpl implements Accounts {
|
||||
if (checkNotNull(in, "AccountInput").username == null) {
|
||||
throw new BadRequestException("AccountInput must specify username");
|
||||
}
|
||||
checkRequiresCapability(self, null, CreateAccount.class);
|
||||
try {
|
||||
AccountInfo info =
|
||||
createAccount.create(in.username).apply(TopLevelResource.INSTANCE, in).value();
|
||||
CreateAccount impl = createAccount.create(in.username);
|
||||
permissionBackend.user(self).checkAny(GlobalPermission.fromAnnotation(impl.getClass()));
|
||||
AccountInfo info = impl.apply(TopLevelResource.INSTANCE, in).value();
|
||||
return id(info._accountId);
|
||||
} catch (OrmException | IOException | ConfigInvalidException e) {
|
||||
} catch (OrmException | IOException | ConfigInvalidException | PermissionBackendException e) {
|
||||
throw new RestApiException("Cannot create account " + in.username, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
package com.google.gerrit.server.api.groups;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.gerrit.server.account.CapabilityUtils.checkRequiresCapability;
|
||||
|
||||
import com.google.gerrit.extensions.api.groups.GroupApi;
|
||||
import com.google.gerrit.extensions.api.groups.GroupInput;
|
||||
@@ -32,6 +31,9 @@ import com.google.gerrit.server.group.CreateGroup;
|
||||
import com.google.gerrit.server.group.GroupsCollection;
|
||||
import com.google.gerrit.server.group.ListGroups;
|
||||
import com.google.gerrit.server.group.QueryGroups;
|
||||
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.project.ProjectsCollection;
|
||||
import com.google.gwtorm.server.OrmException;
|
||||
import com.google.inject.Inject;
|
||||
@@ -49,6 +51,7 @@ class GroupsImpl implements Groups {
|
||||
private final Provider<ListGroups> listGroups;
|
||||
private final Provider<QueryGroups> queryGroups;
|
||||
private final Provider<CurrentUser> user;
|
||||
private final PermissionBackend permissionBackend;
|
||||
private final CreateGroup.Factory createGroup;
|
||||
private final GroupApiImpl.Factory api;
|
||||
|
||||
@@ -60,6 +63,7 @@ class GroupsImpl implements Groups {
|
||||
Provider<ListGroups> listGroups,
|
||||
Provider<QueryGroups> queryGroups,
|
||||
Provider<CurrentUser> user,
|
||||
PermissionBackend permissionBackend,
|
||||
CreateGroup.Factory createGroup,
|
||||
GroupApiImpl.Factory api) {
|
||||
this.accounts = accounts;
|
||||
@@ -68,6 +72,7 @@ class GroupsImpl implements Groups {
|
||||
this.listGroups = listGroups;
|
||||
this.queryGroups = queryGroups;
|
||||
this.user = user;
|
||||
this.permissionBackend = permissionBackend;
|
||||
this.createGroup = createGroup;
|
||||
this.api = api;
|
||||
}
|
||||
@@ -89,11 +94,12 @@ class GroupsImpl implements Groups {
|
||||
if (checkNotNull(in, "GroupInput").name == null) {
|
||||
throw new BadRequestException("GroupInput must specify name");
|
||||
}
|
||||
checkRequiresCapability(user, null, CreateGroup.class);
|
||||
try {
|
||||
GroupInfo info = createGroup.create(in.name).apply(TopLevelResource.INSTANCE, in);
|
||||
CreateGroup impl = createGroup.create(in.name);
|
||||
permissionBackend.user(user).checkAny(GlobalPermission.fromAnnotation(impl.getClass()));
|
||||
GroupInfo info = impl.apply(TopLevelResource.INSTANCE, in);
|
||||
return id(info.id);
|
||||
} catch (OrmException | IOException e) {
|
||||
} catch (OrmException | IOException | PermissionBackendException e) {
|
||||
throw new RestApiException("Cannot create group " + in.name, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
|
||||
package com.google.gerrit.server.api.projects;
|
||||
|
||||
import static com.google.gerrit.server.account.CapabilityUtils.checkRequiresCapability;
|
||||
|
||||
import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
|
||||
import com.google.gerrit.extensions.api.access.ProjectAccessInput;
|
||||
import com.google.gerrit.extensions.api.projects.BranchApi;
|
||||
@@ -39,6 +37,9 @@ import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
|
||||
import com.google.gerrit.extensions.restapi.RestApiException;
|
||||
import com.google.gerrit.extensions.restapi.TopLevelResource;
|
||||
import com.google.gerrit.server.CurrentUser;
|
||||
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.project.ChildProjectsCollection;
|
||||
import com.google.gerrit.server.project.CommitsCollection;
|
||||
import com.google.gerrit.server.project.CreateProject;
|
||||
@@ -71,6 +72,7 @@ public class ProjectApiImpl implements ProjectApi {
|
||||
}
|
||||
|
||||
private final CurrentUser user;
|
||||
private final PermissionBackend permissionBackend;
|
||||
private final CreateProject.Factory createProjectFactory;
|
||||
private final ProjectApiImpl.Factory projectApi;
|
||||
private final ProjectsCollection projects;
|
||||
@@ -97,6 +99,7 @@ public class ProjectApiImpl implements ProjectApi {
|
||||
@AssistedInject
|
||||
ProjectApiImpl(
|
||||
CurrentUser user,
|
||||
PermissionBackend permissionBackend,
|
||||
CreateProject.Factory createProjectFactory,
|
||||
ProjectApiImpl.Factory projectApi,
|
||||
ProjectsCollection projects,
|
||||
@@ -120,6 +123,7 @@ public class ProjectApiImpl implements ProjectApi {
|
||||
@Assisted ProjectResource project) {
|
||||
this(
|
||||
user,
|
||||
permissionBackend,
|
||||
createProjectFactory,
|
||||
projectApi,
|
||||
projects,
|
||||
@@ -147,6 +151,7 @@ public class ProjectApiImpl implements ProjectApi {
|
||||
@AssistedInject
|
||||
ProjectApiImpl(
|
||||
CurrentUser user,
|
||||
PermissionBackend permissionBackend,
|
||||
CreateProject.Factory createProjectFactory,
|
||||
ProjectApiImpl.Factory projectApi,
|
||||
ProjectsCollection projects,
|
||||
@@ -170,6 +175,7 @@ public class ProjectApiImpl implements ProjectApi {
|
||||
@Assisted String name) {
|
||||
this(
|
||||
user,
|
||||
permissionBackend,
|
||||
createProjectFactory,
|
||||
projectApi,
|
||||
projects,
|
||||
@@ -196,6 +202,7 @@ public class ProjectApiImpl implements ProjectApi {
|
||||
|
||||
private ProjectApiImpl(
|
||||
CurrentUser user,
|
||||
PermissionBackend permissionBackend,
|
||||
CreateProject.Factory createProjectFactory,
|
||||
ProjectApiImpl.Factory projectApi,
|
||||
ProjectsCollection projects,
|
||||
@@ -219,6 +226,7 @@ public class ProjectApiImpl implements ProjectApi {
|
||||
CommitApiImpl.Factory commitApi,
|
||||
String name) {
|
||||
this.user = user;
|
||||
this.permissionBackend = permissionBackend;
|
||||
this.createProjectFactory = createProjectFactory;
|
||||
this.projectApi = projectApi;
|
||||
this.projects = projects;
|
||||
@@ -257,10 +265,11 @@ public class ProjectApiImpl implements ProjectApi {
|
||||
if (in.name != null && !name.equals(in.name)) {
|
||||
throw new BadRequestException("name must match input.name");
|
||||
}
|
||||
checkRequiresCapability(user, null, CreateProject.class);
|
||||
createProjectFactory.create(name).apply(TopLevelResource.INSTANCE, in);
|
||||
CreateProject impl = createProjectFactory.create(name);
|
||||
permissionBackend.user(user).checkAny(GlobalPermission.fromAnnotation(impl.getClass()));
|
||||
impl.apply(TopLevelResource.INSTANCE, in);
|
||||
return projectApi.create(projects.parse(name));
|
||||
} catch (IOException | ConfigInvalidException e) {
|
||||
} catch (IOException | ConfigInvalidException | PermissionBackendException e) {
|
||||
throw new RestApiException("Cannot create project: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,14 +30,11 @@ import com.google.gerrit.extensions.registration.DynamicSet;
|
||||
import com.google.gerrit.extensions.restapi.RestView;
|
||||
import com.google.gerrit.extensions.webui.PrivateInternals_UiActionDescription;
|
||||
import com.google.gerrit.extensions.webui.UiAction;
|
||||
import com.google.gerrit.server.CurrentUser;
|
||||
import com.google.gerrit.server.extensions.webui.UiActions;
|
||||
import com.google.gerrit.server.project.ChangeControl;
|
||||
import com.google.gwtorm.server.OrmException;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.Singleton;
|
||||
import com.google.inject.util.Providers;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@@ -48,6 +45,7 @@ public class ActionJson {
|
||||
private final Revisions revisions;
|
||||
private final ChangeJson.Factory changeJsonFactory;
|
||||
private final ChangeResource.Factory changeResourceFactory;
|
||||
private final UiActions uiActions;
|
||||
private final DynamicMap<RestView<ChangeResource>> changeViews;
|
||||
private final DynamicSet<ActionVisitor> visitorSet;
|
||||
|
||||
@@ -56,11 +54,13 @@ public class ActionJson {
|
||||
Revisions revisions,
|
||||
ChangeJson.Factory changeJsonFactory,
|
||||
ChangeResource.Factory changeResourceFactory,
|
||||
UiActions uiActions,
|
||||
DynamicMap<RestView<ChangeResource>> changeViews,
|
||||
DynamicSet<ActionVisitor> visitorSet) {
|
||||
this.revisions = revisions;
|
||||
this.changeJsonFactory = changeJsonFactory;
|
||||
this.changeResourceFactory = changeResourceFactory;
|
||||
this.uiActions = uiActions;
|
||||
this.changeViews = changeViews;
|
||||
this.visitorSet = visitorSet;
|
||||
}
|
||||
@@ -162,9 +162,9 @@ public class ActionJson {
|
||||
return out;
|
||||
}
|
||||
|
||||
Provider<CurrentUser> userProvider = Providers.of(ctl.getUser());
|
||||
FluentIterable<UiAction.Description> descs =
|
||||
UiActions.from(changeViews, changeResourceFactory.create(ctl), userProvider);
|
||||
uiActions.from(changeViews, changeResourceFactory.create(ctl));
|
||||
|
||||
// The followup action is a client-side only operation that does not
|
||||
// have a server side handler. It must be manually registered into the
|
||||
// resulting action map.
|
||||
@@ -198,10 +198,10 @@ public class ActionJson {
|
||||
if (!rsrc.getControl().getUser().isIdentifiedUser()) {
|
||||
return ImmutableMap.of();
|
||||
}
|
||||
|
||||
Map<String, ActionInfo> out = new LinkedHashMap<>();
|
||||
Provider<CurrentUser> userProvider = Providers.of(rsrc.getControl().getUser());
|
||||
ACTION:
|
||||
for (UiAction.Description d : UiActions.from(revisions, rsrc, userProvider)) {
|
||||
for (UiAction.Description d : uiActions.from(revisions, rsrc)) {
|
||||
ActionInfo actionInfo = new ActionInfo(d);
|
||||
for (ActionVisitor visitor : visitors) {
|
||||
if (!visitor.visit(d.getId(), actionInfo, changeInfo, revisionInfo)) {
|
||||
|
||||
@@ -107,6 +107,7 @@ import com.google.gerrit.server.change.ReviewerSuggestion;
|
||||
import com.google.gerrit.server.events.EventFactory;
|
||||
import com.google.gerrit.server.events.EventsMetrics;
|
||||
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
|
||||
import com.google.gerrit.server.extensions.webui.UiActions;
|
||||
import com.google.gerrit.server.git.AbandonOp;
|
||||
import com.google.gerrit.server.git.ChangeMessageModifier;
|
||||
import com.google.gerrit.server.git.EmailMerge;
|
||||
@@ -293,6 +294,7 @@ public class GerritGlobalModule extends FactoryModule {
|
||||
bind(AccountControl.Factory.class);
|
||||
|
||||
install(new AuditModule());
|
||||
bind(UiActions.class);
|
||||
install(new com.google.gerrit.server.access.Module());
|
||||
install(new com.google.gerrit.server.account.Module());
|
||||
install(new com.google.gerrit.server.api.Module());
|
||||
|
||||
@@ -16,20 +16,27 @@ package com.google.gerrit.server.extensions.webui;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.FluentIterable;
|
||||
import com.google.gerrit.common.Nullable;
|
||||
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||
import com.google.gerrit.extensions.restapi.AuthException;
|
||||
import com.google.gerrit.extensions.restapi.RestCollection;
|
||||
import com.google.gerrit.extensions.restapi.RestResource;
|
||||
import com.google.gerrit.extensions.restapi.RestView;
|
||||
import com.google.gerrit.extensions.webui.PrivateInternals_UiActionDescription;
|
||||
import com.google.gerrit.extensions.webui.UiAction;
|
||||
import com.google.gerrit.server.CurrentUser;
|
||||
import com.google.gerrit.server.account.CapabilityUtils;
|
||||
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.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.Singleton;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@Singleton
|
||||
public class UiActions {
|
||||
private static final Logger log = LoggerFactory.getLogger(UiActions.class);
|
||||
|
||||
@@ -37,57 +44,70 @@ public class UiActions {
|
||||
return UiAction.Description::isEnabled;
|
||||
}
|
||||
|
||||
public static <R extends RestResource> FluentIterable<UiAction.Description> from(
|
||||
RestCollection<?, R> collection, R resource, Provider<CurrentUser> userProvider) {
|
||||
return from(collection.views(), resource, userProvider);
|
||||
private final PermissionBackend permissionBackend;
|
||||
private final Provider<CurrentUser> userProvider;
|
||||
|
||||
@Inject
|
||||
UiActions(PermissionBackend permissionBackend, Provider<CurrentUser> userProvider) {
|
||||
this.permissionBackend = permissionBackend;
|
||||
this.userProvider = userProvider;
|
||||
}
|
||||
|
||||
public static <R extends RestResource> FluentIterable<UiAction.Description> from(
|
||||
DynamicMap<RestView<R>> views, R resource, Provider<CurrentUser> userProvider) {
|
||||
public <R extends RestResource> FluentIterable<UiAction.Description> from(
|
||||
RestCollection<?, R> collection, R resource) {
|
||||
return from(collection.views(), resource);
|
||||
}
|
||||
|
||||
public <R extends RestResource> FluentIterable<UiAction.Description> from(
|
||||
DynamicMap<RestView<R>> views, R resource) {
|
||||
return FluentIterable.from(views)
|
||||
.transform(
|
||||
(DynamicMap.Entry<RestView<R>> e) -> {
|
||||
int d = e.getExportName().indexOf('.');
|
||||
if (d < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
RestView<R> view;
|
||||
try {
|
||||
view = e.getProvider().get();
|
||||
} catch (RuntimeException err) {
|
||||
log.error(
|
||||
String.format(
|
||||
"error creating view %s.%s", e.getPluginName(), e.getExportName()),
|
||||
err);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(view instanceof UiAction)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
CapabilityUtils.checkRequiresCapability(
|
||||
userProvider, e.getPluginName(), view.getClass());
|
||||
} catch (AuthException exc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
UiAction.Description dsc = ((UiAction<R>) view).getDescription(resource);
|
||||
if (dsc == null || !dsc.isVisible()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String name = e.getExportName().substring(d + 1);
|
||||
PrivateInternals_UiActionDescription.setMethod(
|
||||
dsc, e.getExportName().substring(0, d));
|
||||
PrivateInternals_UiActionDescription.setId(
|
||||
dsc, "gerrit".equals(e.getPluginName()) ? name : e.getPluginName() + '~' + name);
|
||||
return dsc;
|
||||
})
|
||||
.transform((e) -> describe(e, resource))
|
||||
.filter(Objects::nonNull);
|
||||
}
|
||||
|
||||
private UiActions() {}
|
||||
@Nullable
|
||||
private <R extends RestResource> UiAction.Description describe(
|
||||
DynamicMap.Entry<RestView<R>> e, R resource) {
|
||||
int d = e.getExportName().indexOf('.');
|
||||
if (d < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
RestView<R> view;
|
||||
try {
|
||||
view = e.getProvider().get();
|
||||
} catch (RuntimeException err) {
|
||||
log.error(
|
||||
String.format("error creating view %s.%s", e.getPluginName(), e.getExportName()), err);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(view instanceof UiAction)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
Set<GlobalOrPluginPermission> need =
|
||||
GlobalPermission.fromAnnotation(e.getPluginName(), view.getClass());
|
||||
if (!need.isEmpty() && permissionBackend.user(userProvider).test(need).isEmpty()) {
|
||||
// A permission is required, but test returned no candidates.
|
||||
return null;
|
||||
}
|
||||
} catch (PermissionBackendException err) {
|
||||
log.error(
|
||||
String.format("exception testing view %s.%s", e.getPluginName(), e.getExportName()), err);
|
||||
return null;
|
||||
}
|
||||
|
||||
UiAction.Description dsc = ((UiAction<R>) view).getDescription(resource);
|
||||
if (dsc == null || !dsc.isVisible()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String name = e.getExportName().substring(d + 1);
|
||||
PrivateInternals_UiActionDescription.setMethod(dsc, e.getExportName().substring(0, d));
|
||||
PrivateInternals_UiActionDescription.setId(
|
||||
dsc, "gerrit".equals(e.getPluginName()) ? name : e.getPluginName() + '~' + name);
|
||||
return dsc;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,16 @@ 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 com.google.gerrit.extensions.annotations.CapabilityScope;
|
||||
import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
|
||||
import com.google.gerrit.extensions.annotations.RequiresCapability;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/** Global server permissions built into Gerrit. */
|
||||
public enum GlobalPermission implements GlobalOrPluginPermission {
|
||||
@@ -40,6 +49,7 @@ public enum GlobalPermission implements GlobalOrPluginPermission {
|
||||
VIEW_PLUGINS(GlobalCapability.VIEW_PLUGINS),
|
||||
VIEW_QUEUE(GlobalCapability.VIEW_QUEUE);
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(GlobalPermission.class);
|
||||
private static final ImmutableMap<String, GlobalPermission> BY_NAME;
|
||||
|
||||
static {
|
||||
@@ -55,6 +65,47 @@ public enum GlobalPermission implements GlobalOrPluginPermission {
|
||||
return BY_NAME.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the {@code @RequiresCapability} or {@code @RequiresAnyCapability} annotation.
|
||||
*
|
||||
* @param pluginName name of the declaring plugin. May be {@code null} or {@code "gerrit"} for
|
||||
* classes originating from the core server.
|
||||
* @param clazz target class to extract annotation from.
|
||||
* @return empty set if no annotations were found, or a collection of permissions, any of which
|
||||
* are suitable to enable access.
|
||||
* @throws PermissionBackendException the annotation could not be parsed.
|
||||
*/
|
||||
public static Set<GlobalOrPluginPermission> fromAnnotation(
|
||||
@Nullable String pluginName, Class<?> clazz) throws PermissionBackendException {
|
||||
RequiresCapability rc = findAnnotation(clazz, RequiresCapability.class);
|
||||
RequiresAnyCapability rac = findAnnotation(clazz, RequiresAnyCapability.class);
|
||||
if (rc != null && rac != null) {
|
||||
log.error(
|
||||
String.format(
|
||||
"Class %s uses both @%s and @%s",
|
||||
clazz.getName(),
|
||||
RequiresCapability.class.getSimpleName(),
|
||||
RequiresAnyCapability.class.getSimpleName()));
|
||||
throw new PermissionBackendException("cannot extract permission");
|
||||
} else if (rc != null) {
|
||||
return Collections.singleton(
|
||||
resolve(pluginName, rc.value(), rc.scope(), clazz, RequiresCapability.class));
|
||||
} else if (rac != null) {
|
||||
Set<GlobalOrPluginPermission> r = new LinkedHashSet<>();
|
||||
for (String capability : rac.value()) {
|
||||
r.add(resolve(pluginName, capability, rac.scope(), clazz, RequiresAnyCapability.class));
|
||||
}
|
||||
return Collections.unmodifiableSet(r);
|
||||
} else {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
}
|
||||
|
||||
public static Set<GlobalOrPluginPermission> fromAnnotation(Class<?> clazz)
|
||||
throws PermissionBackendException {
|
||||
return fromAnnotation(null, clazz);
|
||||
}
|
||||
|
||||
private final String name;
|
||||
|
||||
GlobalPermission(String name) {
|
||||
@@ -71,4 +122,45 @@ public enum GlobalPermission implements GlobalOrPluginPermission {
|
||||
public String describeForException() {
|
||||
return toString().toLowerCase(Locale.US).replace('_', ' ');
|
||||
}
|
||||
|
||||
private static GlobalOrPluginPermission resolve(
|
||||
@Nullable String pluginName,
|
||||
String capability,
|
||||
CapabilityScope scope,
|
||||
Class<?> clazz,
|
||||
Class<?> annotationClass)
|
||||
throws PermissionBackendException {
|
||||
if (pluginName != null
|
||||
&& !"gerrit".equals(pluginName)
|
||||
&& (scope == CapabilityScope.PLUGIN || scope == CapabilityScope.CONTEXT)) {
|
||||
return new PluginPermission(pluginName, capability);
|
||||
}
|
||||
|
||||
if (scope == CapabilityScope.PLUGIN) {
|
||||
log.error(
|
||||
String.format(
|
||||
"Class %s uses @%s(scope=%s), but is not within a plugin",
|
||||
clazz.getName(), annotationClass.getSimpleName(), scope.name()));
|
||||
throw new PermissionBackendException("cannot extract permission");
|
||||
}
|
||||
|
||||
GlobalPermission perm = byName(capability);
|
||||
if (perm == null) {
|
||||
log.error(
|
||||
String.format("Class %s requires unknown capability %s", clazz.getName(), capability));
|
||||
throw new PermissionBackendException("cannot extract permission");
|
||||
}
|
||||
return perm;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static <T extends Annotation> T findAnnotation(Class<?> clazz, Class<T> annotation) {
|
||||
for (; clazz != null; clazz = clazz.getSuperclass()) {
|
||||
T t = clazz.getAnnotation(annotation);
|
||||
if (t != null) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ 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.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -137,6 +138,35 @@ public abstract class PermissionBackend {
|
||||
public abstract void check(GlobalOrPluginPermission perm)
|
||||
throws AuthException, PermissionBackendException;
|
||||
|
||||
/**
|
||||
* Verify scoped user can perform at least one listed permission.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>If the user has at least one of the permissions in {@code any}, the method completes
|
||||
* normally, possibly without checking all listed permissions.
|
||||
*
|
||||
* <p>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<GlobalOrPluginPermission> any)
|
||||
throws PermissionBackendException, AuthException {
|
||||
for (Iterator<GlobalOrPluginPermission> 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 <T extends GlobalOrPluginPermission> Set<T> test(Collection<T> permSet)
|
||||
throws PermissionBackendException;
|
||||
|
||||
@@ -31,7 +31,6 @@ import com.google.gerrit.server.config.PluginConfigFactory;
|
||||
import com.google.gerrit.server.config.ProjectConfigEntry;
|
||||
import com.google.gerrit.server.extensions.webui.UiActions;
|
||||
import com.google.gerrit.server.git.TransferConfig;
|
||||
import com.google.inject.util.Providers;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
@@ -45,6 +44,7 @@ public class ConfigInfoImpl extends ConfigInfo {
|
||||
DynamicMap<ProjectConfigEntry> pluginConfigEntries,
|
||||
PluginConfigFactory cfgFactory,
|
||||
AllProjectsName allProjects,
|
||||
UiActions uiActions,
|
||||
DynamicMap<RestView<ProjectResource>> views) {
|
||||
ProjectState projectState = control.getProjectState();
|
||||
Project p = control.getProject();
|
||||
@@ -126,8 +126,7 @@ public class ConfigInfoImpl extends ConfigInfo {
|
||||
getPluginConfig(control.getProjectState(), pluginConfigEntries, cfgFactory, allProjects);
|
||||
|
||||
actions = new TreeMap<>();
|
||||
for (UiAction.Description d :
|
||||
UiActions.from(views, new ProjectResource(control), Providers.of(control.getUser()))) {
|
||||
for (UiAction.Description d : uiActions.from(views, new ProjectResource(control))) {
|
||||
actions.put(d.getId(), new ActionInfo(d));
|
||||
}
|
||||
this.theme = projectState.getTheme();
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.google.gerrit.server.EnableSignedPush;
|
||||
import com.google.gerrit.server.config.AllProjectsName;
|
||||
import com.google.gerrit.server.config.PluginConfigFactory;
|
||||
import com.google.gerrit.server.config.ProjectConfigEntry;
|
||||
import com.google.gerrit.server.extensions.webui.UiActions;
|
||||
import com.google.gerrit.server.git.TransferConfig;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
@@ -33,6 +34,7 @@ public class GetConfig implements RestReadView<ProjectResource> {
|
||||
private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
|
||||
private final PluginConfigFactory cfgFactory;
|
||||
private final AllProjectsName allProjects;
|
||||
private final UiActions uiActions;
|
||||
private final DynamicMap<RestView<ProjectResource>> views;
|
||||
|
||||
@Inject
|
||||
@@ -42,12 +44,14 @@ public class GetConfig implements RestReadView<ProjectResource> {
|
||||
DynamicMap<ProjectConfigEntry> pluginConfigEntries,
|
||||
PluginConfigFactory cfgFactory,
|
||||
AllProjectsName allProjects,
|
||||
UiActions uiActions,
|
||||
DynamicMap<RestView<ProjectResource>> views) {
|
||||
this.serverEnableSignedPush = serverEnableSignedPush;
|
||||
this.config = config;
|
||||
this.pluginConfigEntries = pluginConfigEntries;
|
||||
this.allProjects = allProjects;
|
||||
this.cfgFactory = cfgFactory;
|
||||
this.uiActions = uiActions;
|
||||
this.views = views;
|
||||
}
|
||||
|
||||
@@ -60,6 +64,7 @@ public class GetConfig implements RestReadView<ProjectResource> {
|
||||
pluginConfigEntries,
|
||||
cfgFactory,
|
||||
allProjects,
|
||||
uiActions,
|
||||
views);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ import com.google.gerrit.server.WebLinks;
|
||||
import com.google.gerrit.server.extensions.webui.UiActions;
|
||||
import com.google.gerrit.server.git.GitRepositoryManager;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.util.Providers;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
@@ -48,6 +47,7 @@ import org.kohsuke.args4j.Option;
|
||||
public class ListBranches implements RestReadView<ProjectResource> {
|
||||
private final GitRepositoryManager repoManager;
|
||||
private final DynamicMap<RestView<BranchResource>> branchViews;
|
||||
private final UiActions uiActions;
|
||||
private final WebLinks webLinks;
|
||||
|
||||
@Option(
|
||||
@@ -99,9 +99,11 @@ public class ListBranches implements RestReadView<ProjectResource> {
|
||||
public ListBranches(
|
||||
GitRepositoryManager repoManager,
|
||||
DynamicMap<RestView<BranchResource>> branchViews,
|
||||
UiActions uiActions,
|
||||
WebLinks webLinks) {
|
||||
this.repoManager = repoManager;
|
||||
this.branchViews = branchViews;
|
||||
this.uiActions = uiActions;
|
||||
this.webLinks = webLinks;
|
||||
}
|
||||
|
||||
@@ -197,16 +199,15 @@ public class ListBranches implements RestReadView<ProjectResource> {
|
||||
info.ref = ref.getName();
|
||||
info.revision = ref.getObjectId() != null ? ref.getObjectId().name() : null;
|
||||
info.canDelete = !targets.contains(ref.getName()) && refControl.canDelete() ? true : null;
|
||||
for (UiAction.Description d :
|
||||
UiActions.from(
|
||||
branchViews,
|
||||
new BranchResource(refControl.getProjectControl(), info),
|
||||
Providers.of(refControl.getUser()))) {
|
||||
|
||||
BranchResource rsrc = new BranchResource(refControl.getProjectControl(), info);
|
||||
for (UiAction.Description d : uiActions.from(branchViews, rsrc)) {
|
||||
if (info.actions == null) {
|
||||
info.actions = new TreeMap<>();
|
||||
}
|
||||
info.actions.put(d.getId(), new ActionInfo(d));
|
||||
}
|
||||
|
||||
List<WebLinkInfo> links =
|
||||
webLinks.getBranchLinks(
|
||||
refControl.getProjectControl().getProject().getName(), ref.getName());
|
||||
|
||||
@@ -36,6 +36,7 @@ import com.google.gerrit.server.config.AllProjectsName;
|
||||
import com.google.gerrit.server.config.PluginConfig;
|
||||
import com.google.gerrit.server.config.PluginConfigFactory;
|
||||
import com.google.gerrit.server.config.ProjectConfigEntry;
|
||||
import com.google.gerrit.server.extensions.webui.UiActions;
|
||||
import com.google.gerrit.server.git.MetaDataUpdate;
|
||||
import com.google.gerrit.server.git.ProjectConfig;
|
||||
import com.google.gerrit.server.git.TransferConfig;
|
||||
@@ -64,6 +65,7 @@ public class PutConfig implements RestModifyView<ProjectResource, ConfigInput> {
|
||||
private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
|
||||
private final PluginConfigFactory cfgFactory;
|
||||
private final AllProjectsName allProjects;
|
||||
private final UiActions uiActions;
|
||||
private final DynamicMap<RestView<ProjectResource>> views;
|
||||
private final Provider<CurrentUser> user;
|
||||
|
||||
@@ -77,6 +79,7 @@ public class PutConfig implements RestModifyView<ProjectResource, ConfigInput> {
|
||||
DynamicMap<ProjectConfigEntry> pluginConfigEntries,
|
||||
PluginConfigFactory cfgFactory,
|
||||
AllProjectsName allProjects,
|
||||
UiActions uiActions,
|
||||
DynamicMap<RestView<ProjectResource>> views,
|
||||
Provider<CurrentUser> user) {
|
||||
this.serverEnableSignedPush = serverEnableSignedPush;
|
||||
@@ -87,6 +90,7 @@ public class PutConfig implements RestModifyView<ProjectResource, ConfigInput> {
|
||||
this.pluginConfigEntries = pluginConfigEntries;
|
||||
this.cfgFactory = cfgFactory;
|
||||
this.allProjects = allProjects;
|
||||
this.uiActions = uiActions;
|
||||
this.views = views;
|
||||
this.user = user;
|
||||
}
|
||||
@@ -185,6 +189,7 @@ public class PutConfig implements RestModifyView<ProjectResource, ConfigInput> {
|
||||
pluginConfigEntries,
|
||||
cfgFactory,
|
||||
allProjects,
|
||||
uiActions,
|
||||
views);
|
||||
} catch (RepositoryNotFoundException notFound) {
|
||||
throw new ResourceNotFoundException(projectName.get());
|
||||
|
||||
Reference in New Issue
Block a user