Add support for plugin-owned capabilities

Plugin contributed SSH commands and UiActions can now be annotated
with plugin-owned capabilities.

Capability scope was introduced to differentiate between plugin-owned
capabilities and core capabilities. Per default the scope of
@RequiresCapability annotation is CapabilityScope.CONTEXT, i. e. when
@RequiresCapability is used within a plugin the scope of the capability is
assumed to be that plugin. If @RequiresCapability is used within the core
Gerrit Code Review server (and thus is outside of a plugin) the scope is the
core server and will use the GlobalCapability known to Gerrit Code Review
server.

If a plugin needs to use a core capability name (e.g. "administrateServer")
this can be specified by setting scope = CapabilityScope.CORE:

  @RequiresCapability(value="administrateServer", scope=CapabilityScope.CORE)

Change-Id: I82f7a6fef2a47613a1fd9c7474ff568db3ca84a2
This commit is contained in:
David Ostrovsky
2013-06-15 14:46:23 +02:00
parent 362963311e
commit 7066cc064e
13 changed files with 339 additions and 46 deletions

View File

@@ -310,6 +310,87 @@ by PrintHello class will be available to users as:
$ ssh -p 29418 review.example.com helloworld print
----
[[capabilities]]
Plugin Owned Capabilities
-------------------------
Plugins may provide their own capabilities and restrict usage of SSH
commands to the users who are granted those capabilities.
Plugins define the capabilities by overriding the `CapabilityDefinition`
abstract class:
====
public class PrintHelloCapability extends CapabilityDefinition {
@Override
public String getDescription() {
return "Print Hello";
}
}
====
If no Guice modules are declared in the manifest, UI commands may
use auto-registration by providing an `@Export` annotation:
====
@Export("printHello")
public class PrintHelloCapability extends CapabilityDefinition {
...
====
Otherwise the capability must be bound in a plugin module:
====
public class HelloWorldModule extends AbstractModule {
@Override
protected void configure() {
bind(CapabilityDefinition.class)
.annotatedWith(Exports.named("printHello"))
.to(PrintHelloCapability.class);
}
}
====
With a plugin-owned capability defined in this way, it is possible to restrict
usage of an SSH command or UiAction to members of the group that were granted
this capability in the usual way, using the `RequiresCapability` annotation:
====
@RequiresCapability("printHello")
@CommandMetaData(name="print", descr="Print greeting in different languages")
public final class PrintHelloWorldCommand extends SshCommand {
...
====
Or with UiAction:
====
@RequiresCapability("printHello")
public class SayHelloAction extends UiAction<RevisionResource>
implements RestModifyView<RevisionResource, SayHelloAction.Input> {
...
====
Capability scope was introduced to differentiate between plugin-owned
capabilities and core capabilities. Per default the scope of
@RequiresCapability annotation is `CapabilityScope.CONTEXT`, that means:
+
* when `@RequiresCapability` is used within a plugin the scope of the
capability is assumed to be that plugin.
+
* If `@RequiresCapability` is used within the core Gerrit Code Review server
(and thus is outside of a plugin) the scope is the core server and will use
the `GlobalCapability` known to Gerrit Code Review server.
If a plugin needs to use a core capability name (e.g. "administrateServer")
this can be specified by setting `scope = CapabilityScope.CORE`:
====
@RequiresCapability(value = "administrateServer", scope =
CapabilityScope.CORE)
...
====
[[http]]
HTTP Servlets
-------------

View File

@@ -0,0 +1,36 @@
// 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.extensions.annotations;
/** Declared scope of a capability named by {@link RequiresCapability}. */
public enum CapabilityScope {
/**
* Scope is assumed based on the context.
*
* When {@code @RequiresCapability} is used within a plugin the scope of the
* capability is assumed to be that plugin.
*
* If {@code @RequiresCapability} is used within the core Gerrit Code Review
* server (and thus is outside of a plugin) the scope is the core server and
* will use {@link com.google.gerrit.common.data.GlobalCapability}.
*/
CONTEXT,
/** Scope is only the plugin. */
PLUGIN,
/** Scope is the core server. */
CORE;
}

View File

@@ -27,5 +27,9 @@ import java.lang.annotation.Target;
@Target({ElementType.TYPE})
@Retention(RUNTIME)
public @interface RequiresCapability {
/** Name of the capability required to invoke this action. */
String value();
/** Scope of the named capability. */
CapabilityScope scope() default CapabilityScope.CONTEXT;
}

View File

@@ -0,0 +1,24 @@
// 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.extensions.config;
import com.google.gerrit.extensions.annotations.ExtensionPoint;
/** Specifies a capability declared by a plugin. */
@ExtensionPoint
public abstract class CapabilityDefinition {
/** @return description of the capability. */
public abstract String getDescription();
}

View File

@@ -23,8 +23,8 @@ import static javax.servlet.http.HttpServletResponse.SC_CREATED;
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED;
import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
@@ -46,6 +46,7 @@ import com.google.common.math.IntMath;
import com.google.common.net.HttpHeaders;
import com.google.gerrit.audit.AuditService;
import com.google.gerrit.audit.HttpAuditEvent;
import com.google.gerrit.extensions.annotations.CapabilityScope;
import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.AcceptsCreate;
@@ -199,17 +200,17 @@ public class RestApiServlet extends HttpServlet {
List<IdString> path = splitPath(req);
RestCollection<RestResource, RestResource> rc = members.get();
checkAccessAnnotations(rc.getClass());
checkAccessAnnotations(null, rc.getClass());
RestResource rsrc = TopLevelResource.INSTANCE;
RestView<RestResource> view = null;
ViewData viewData = new ViewData(null, null);
if (path.isEmpty()) {
if ("GET".equals(req.getMethod())) {
view = rc.list();
viewData = new ViewData(null, rc.list());
} else if (rc instanceof AcceptsPost && "POST".equals(req.getMethod())) {
@SuppressWarnings("unchecked")
AcceptsPost<RestResource> ac = (AcceptsPost<RestResource>) rc;
view = ac.post(rsrc);
viewData = new ViewData(null, ac.post(rsrc));
} else {
throw new MethodNotAllowedException();
}
@@ -227,30 +228,30 @@ public class RestApiServlet extends HttpServlet {
|| "PUT".equals(req.getMethod()))) {
@SuppressWarnings("unchecked")
AcceptsCreate<RestResource> ac = (AcceptsCreate<RestResource>) rc;
view = ac.create(rsrc, id);
viewData = new ViewData(null, ac.create(rsrc, id));
status = SC_CREATED;
} else {
throw e;
}
}
if (view == null) {
view = view(rc, req.getMethod(), path);
if (viewData.view == null) {
viewData = view(rc, req.getMethod(), path);
}
}
checkAccessAnnotations(view.getClass());
checkAccessAnnotations(viewData);
while (view instanceof RestCollection<?,?>) {
while (viewData.view instanceof RestCollection<?,?>) {
@SuppressWarnings("unchecked")
RestCollection<RestResource, RestResource> c =
(RestCollection<RestResource, RestResource>) view;
(RestCollection<RestResource, RestResource>) viewData.view;
if (path.isEmpty()) {
if ("GET".equals(req.getMethod())) {
view = c.list();
viewData = new ViewData(null, c.list());
} else if (c instanceof AcceptsPost && "POST".equals(req.getMethod())) {
@SuppressWarnings("unchecked")
AcceptsPost<RestResource> ac = (AcceptsPost<RestResource>) c;
view = ac.post(rsrc);
viewData = new ViewData(null, ac.post(rsrc));
} else {
throw new MethodNotAllowedException();
}
@@ -260,7 +261,7 @@ public class RestApiServlet extends HttpServlet {
try {
rsrc = c.parse(rsrc, id);
checkPreconditions(req, rsrc);
view = null;
viewData = new ViewData(null, null);
} catch (ResourceNotFoundException e) {
if (c instanceof AcceptsCreate
&& path.isEmpty()
@@ -268,17 +269,17 @@ public class RestApiServlet extends HttpServlet {
|| "PUT".equals(req.getMethod()))) {
@SuppressWarnings("unchecked")
AcceptsCreate<RestResource> ac = (AcceptsCreate<RestResource>) c;
view = ac.create(rsrc, id);
viewData = new ViewData(null, ac.create(rsrc, id));
status = SC_CREATED;
} else {
throw e;
}
}
if (view == null) {
view = view(c, req.getMethod(), path);
if (viewData.view == null) {
viewData = view(c, req.getMethod(), path);
}
}
checkAccessAnnotations(view.getClass());
checkAccessAnnotations(viewData);
}
if (notModified(req, rsrc)) {
@@ -288,19 +289,19 @@ public class RestApiServlet extends HttpServlet {
Multimap<String, String> config = LinkedHashMultimap.create();
ParameterParser.splitQueryString(req.getQueryString(), config, params);
if (!globals.paramParser.get().parse(view, params, req, res)) {
if (!globals.paramParser.get().parse(viewData.view, params, req, res)) {
return;
}
if (view instanceof RestModifyView<?, ?>) {
if (viewData.view instanceof RestModifyView<?, ?>) {
@SuppressWarnings("unchecked")
RestModifyView<RestResource, Object> m =
(RestModifyView<RestResource, Object>) view;
(RestModifyView<RestResource, Object>) viewData.view;
inputRequestBody = parseRequest(req, inputType(m));
result = m.apply(rsrc, inputRequestBody);
} else if (view instanceof RestReadView<?>) {
result = ((RestReadView<RestResource>) view).apply(rsrc);
} else if (viewData.view instanceof RestReadView<?>) {
result = ((RestReadView<RestResource>) viewData.view).apply(rsrc);
} else {
throw new ResourceNotFoundException();
}
@@ -766,7 +767,7 @@ public class RestApiServlet extends HttpServlet {
return gz.setContentType(src.getContentType());
}
private RestView<RestResource> view(
private ViewData view(
RestCollection<RestResource, RestResource> rc,
String method, List<IdString> path) throws ResourceNotFoundException,
MethodNotAllowedException, AmbiguousViewException {
@@ -786,7 +787,7 @@ public class RestApiServlet extends HttpServlet {
RestView<RestResource> view =
views.get(p.get(0), method + "." + p.get(1));
if (view != null) {
return view;
return new ViewData(p.get(0), view);
}
throw new ResourceNotFoundException(projection);
}
@@ -794,7 +795,7 @@ public class RestApiServlet extends HttpServlet {
String name = method + "." + p.get(0);
RestView<RestResource> core = views.get("gerrit", name);
if (core != null) {
return core;
return new ViewData(null, core);
}
Map<String, RestView<RestResource>> r = Maps.newTreeMap();
@@ -806,7 +807,9 @@ public class RestApiServlet extends HttpServlet {
}
if (r.size() == 1) {
return Iterables.getFirst(r.values(), null);
Map.Entry<String, RestView<RestResource>> entry =
Iterables.getOnlyElement(r.entrySet());
return new ViewData(entry.getKey(), entry.getValue());
} else if (r.isEmpty()) {
throw new ResourceNotFoundException(projection);
} else {
@@ -862,16 +865,35 @@ public class RestApiServlet extends HttpServlet {
return !("GET".equals(method) || "HEAD".equals(method));
}
private void checkAccessAnnotations(Class<? extends Object> clazz)
private void checkAccessAnnotations(ViewData viewData) throws AuthException {
checkAccessAnnotations(viewData.pluginName, viewData.view.getClass());
}
private void checkAccessAnnotations(String pluginName, Class<?> clazz)
throws AuthException {
RequiresCapability rc = clazz.getAnnotation(RequiresCapability.class);
if (rc != null) {
CurrentUser user = globals.currentUser.get();
CapabilityControl ctl = user.getCapabilities();
if (!ctl.canPerform(rc.value()) && !ctl.canAdministrateServer()) {
String capability = rc.value();
if (pluginName != null && !"gerrit".equals(pluginName)
&& (rc.scope() == CapabilityScope.PLUGIN
|| rc.scope() == CapabilityScope.CONTEXT)) {
capability = String.format("%s-%s", pluginName, rc.value());
} else if (rc.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");
}
if (!ctl.canPerform(capability) && !ctl.canAdministrateServer()) {
throw new AuthException(String.format(
"Capability %s is required to access this resource",
rc.value()));
capability));
}
}
}
@@ -988,4 +1010,14 @@ public class RestApiServlet extends HttpServlet {
super(message);
}
}
private static class ViewData {
String pluginName;
RestView<RestResource> view;
ViewData(String pluginName, RestView<RestResource> view) {
this.pluginName = pluginName;
this.view = view;
}
}
}

View File

@@ -34,6 +34,8 @@ import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.data.PermissionRange;
import com.google.gerrit.extensions.config.CapabilityDefinition;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.BinaryResult;
@@ -68,10 +70,13 @@ class GetCapabilities implements RestReadView<AccountResource> {
private Set<String> query;
private final Provider<CurrentUser> self;
private final DynamicMap<CapabilityDefinition> pluginCapabilities;
@Inject
GetCapabilities(Provider<CurrentUser> self) {
GetCapabilities(Provider<CurrentUser> self,
DynamicMap<CapabilityDefinition> pluginCapabilities) {
this.self = self;
this.pluginCapabilities = pluginCapabilities;
}
@Override
@@ -93,6 +98,14 @@ class GetCapabilities implements RestReadView<AccountResource> {
}
}
}
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);
}
}
}
have.put(CREATE_ACCOUNT, cc.canCreateAccount());
have.put(CREATE_GROUP, cc.canCreateGroup());

View File

@@ -19,6 +19,7 @@ import static com.google.inject.Scopes.SINGLETON;
import com.google.common.cache.Cache;
import com.google.gerrit.audit.AuditModule;
import com.google.gerrit.common.ChangeListener;
import com.google.gerrit.extensions.config.CapabilityDefinition;
import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
import com.google.gerrit.extensions.events.NewProjectCreatedListener;
import com.google.gerrit.extensions.registration.DynamicItem;
@@ -246,6 +247,7 @@ public class GerritGlobalModule extends FactoryModule {
bind(GitReferenceUpdated.class);
DynamicMap.mapOf(binder(), new TypeLiteral<Cache<?, ?>>() {});
DynamicSet.setOf(binder(), CacheRemovalListener.class);
DynamicMap.mapOf(binder(), CapabilityDefinition.class);
DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class);
DynamicSet.setOf(binder(), NewProjectCreatedListener.class);
DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(ChangeCache.class);

View File

@@ -16,21 +16,39 @@ package com.google.gerrit.server.config;
import com.google.common.collect.Maps;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.config.CapabilityDefinition;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.util.Map;
/** List capabilities visible to the calling user. */
public class ListCapabilities implements RestReadView<ConfigResource> {
private final DynamicMap<CapabilityDefinition> pluginCapabilities;
@Inject
public ListCapabilities(DynamicMap<CapabilityDefinition> pluginCapabilities) {
this.pluginCapabilities = pluginCapabilities;
}
@Override
public Map<String, CapabilityInfo> apply(ConfigResource resource)
throws AuthException, BadRequestException, ResourceConflictException,
IllegalArgumentException, SecurityException, IllegalAccessException,
NoSuchFieldException {
Map<String, CapabilityInfo> output = Maps.newTreeMap();
collectCoreCapabilities(output);
collectPluginCapabilities(output);
return output;
}
private void collectCoreCapabilities(Map<String, CapabilityInfo> output)
throws IllegalAccessException, NoSuchFieldException {
Class<? extends CapabilityConstants> bundleClass =
CapabilityConstants.get().getClass();
CapabilityConstants c = CapabilityConstants.get();
@@ -38,7 +56,18 @@ public class ListCapabilities implements RestReadView<ConfigResource> {
String name = (String) bundleClass.getField(id).get(c);
output.put(id, new CapabilityInfo(id, name));
}
return output;
}
private void collectPluginCapabilities(Map<String, CapabilityInfo> output) {
for (String pluginName : pluginCapabilities.plugins()) {
for (Map.Entry<String, Provider<CapabilityDefinition>> entry :
pluginCapabilities.byPlugin(pluginName).entrySet()) {
String id = String.format("%s-%s", pluginName, entry.getKey());
output.put(id, new CapabilityInfo(
id,
entry.getValue().get().getDescription()));
}
}
}
public static class CapabilityInfo {

View File

@@ -533,15 +533,13 @@ public class ProjectConfig extends VersionedMetaData {
AccessSection capability = null;
for (String varName : rc.getNames(CAPABILITY)) {
if (GlobalCapability.isCapability(varName)) {
if (capability == null) {
capability = new AccessSection(AccessSection.GLOBAL_CAPABILITIES);
accessSections.put(AccessSection.GLOBAL_CAPABILITIES, capability);
}
Permission perm = capability.getPermission(varName, true);
loadPermissionRules(rc, CAPABILITY, null, varName, groupsByName, perm,
GlobalCapability.hasRange(varName));
if (capability == null) {
capability = new AccessSection(AccessSection.GLOBAL_CAPABILITIES);
accessSections.put(AccessSection.GLOBAL_CAPABILITIES, capability);
}
Permission perm = capability.getPermission(varName, true);
loadPermissionRules(rc, CAPABILITY, null, varName, groupsByName, perm,
GlobalCapability.hasRange(varName));
}
}
@@ -879,8 +877,7 @@ public class ProjectConfig extends VersionedMetaData {
rc.setStringList(CAPABILITY, null, permission.getName(), rules);
}
for (String varName : rc.getNames(CAPABILITY)) {
if (GlobalCapability.isCapability(varName)
&& !have.contains(varName.toLowerCase())) {
if (!have.contains(varName.toLowerCase())) {
rc.unset(CAPABILITY, null, varName);
}
}

View File

@@ -19,21 +19,55 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.annotations.Exports;
import com.google.gerrit.extensions.config.CapabilityDefinition;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.server.config.ListCapabilities.CapabilityInfo;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import org.junit.Before;
import org.junit.Test;
import java.util.Map;
public class ListCapabilitiesTest {
private Injector injector;
@Before
public void setUp() throws Exception {
AbstractModule mod = new AbstractModule() {
@Override
protected void configure() {
DynamicMap.mapOf(binder(), CapabilityDefinition.class);
bind(CapabilityDefinition.class)
.annotatedWith(Exports.named("printHello"))
.toInstance(new CapabilityDefinition() {
@Override
public String getDescription() {
return "Print Hello";
}
});
}
};
injector = Guice.createInjector(mod);
}
@Test
public void testList() throws Exception {
Map<String, CapabilityInfo> m =
new ListCapabilities().apply(new ConfigResource());
injector.getInstance(ListCapabilities.class)
.apply(new ConfigResource());
for (String id : GlobalCapability.getAllNames()) {
assertTrue("contains " + id, m.containsKey(id));
assertEquals(id, m.get(id).id);
assertNotNull(id + " has name", m.get(id).name);
}
String pluginCapability = "gerrit-printHello";
assertTrue("contains " + pluginCapability, m.containsKey(pluginCapability));
assertEquals(pluginCapability, m.get(pluginCapability).id);
assertEquals("Print Hello", m.get(pluginCapability).name);
}
}

View File

@@ -16,6 +16,7 @@ java_library2(
'//lib:guava',
'//lib:gwtorm',
'//lib:jsch',
'//lib:jsr305',
'//lib/commons:codec',
'//lib/guice:guice',
'//lib/guice:guice-assistedinject',

View File

@@ -15,6 +15,7 @@
package com.google.gerrit.sshd;
import com.google.common.util.concurrent.Atomics;
import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.Project.NameKey;
import com.google.gerrit.server.CurrentUser;
@@ -53,6 +54,8 @@ import java.io.UnsupportedEncodingException;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Nullable;
public abstract class BaseCommand implements Command {
private static final Logger log = LoggerFactory.getLogger(BaseCommand.class);
public static final String ENC = "UTF-8";
@@ -90,6 +93,11 @@ public abstract class BaseCommand implements Command {
@Inject
private Provider<SshScope.Context> contextProvider;
/** Commands declared by a plugin can be scoped by the plugin name. */
@Inject(optional = true)
@PluginName
private String pluginName;
/** The task, as scheduled on a worker thread. */
private final AtomicReference<Future<?>> task;
@@ -119,6 +127,11 @@ public abstract class BaseCommand implements Command {
this.exit = callback;
}
@Nullable
String getPluginName() {
return pluginName;
}
String getName() {
return commandName;
}

View File

@@ -17,6 +17,7 @@ package com.google.gerrit.sshd;
import com.google.common.base.Strings;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.Atomics;
import com.google.gerrit.extensions.annotations.CapabilityScope;
import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.CapabilityControl;
@@ -28,6 +29,8 @@ import com.google.inject.assistedinject.Assisted;
import org.apache.sshd.server.Command;
import org.apache.sshd.server.Environment;
import org.kohsuke.args4j.Argument;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.StringWriter;
@@ -40,6 +43,9 @@ import java.util.concurrent.atomic.AtomicReference;
* Command that dispatches to a subcommand from its command table.
*/
final class DispatchCommand extends BaseCommand {
private static final Logger log = LoggerFactory
.getLogger(DispatchCommand.class);
interface Factory {
DispatchCommand create(Map<String, CommandProvider> map);
}
@@ -113,15 +119,36 @@ final class DispatchCommand extends BaseCommand {
}
}
private void checkRequiresCapability(Command cmd) throws UnloggedFailure {
private void checkRequiresCapability(Command cmd)
throws UnloggedFailure {
RequiresCapability rc = cmd.getClass().getAnnotation(RequiresCapability.class);
if (rc != null) {
CurrentUser user = currentUser.get();
CapabilityControl ctl = user.getCapabilities();
if (!ctl.canPerform(rc.value()) && !ctl.canAdministrateServer()) {
String capability = rc.value();
if (cmd instanceof BaseCommand) {
String pluginName = ((BaseCommand) cmd).getPluginName();
if (pluginName != null && !"gerrit".equals(pluginName)
&& (rc.scope() == CapabilityScope.PLUGIN
|| rc.scope() == CapabilityScope.CONTEXT)) {
capability = String.format("%s-%s", pluginName, rc.value());
} else if (rc.scope() == CapabilityScope.PLUGIN) {
log.error(String.format(
"Class %s uses @%s(scope=%s), but is not within a plugin",
cmd.getClass().getName(),
RequiresCapability.class.getSimpleName(),
CapabilityScope.PLUGIN.name()));
throw new UnloggedFailure(
BaseCommand.STATUS_NOT_ADMIN,
"fatal: cannot check capability");
}
}
if (!ctl.canPerform(capability) && !ctl.canAdministrateServer()) {
String msg = String.format(
"fatal: %s does not have \"%s\" capability.",
user.getUserName(), rc.value());
user.getUserName(), capability);
throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
}
}