diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt index 051f9c2eb9..f8a56b530d 100644 --- a/Documentation/dev-plugins.txt +++ b/Documentation/dev-plugins.txt @@ -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 + implements RestModifyView { + ... +==== + +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 ------------- diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/CapabilityScope.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/CapabilityScope.java new file mode 100644 index 0000000000..ede8b8cd74 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/CapabilityScope.java @@ -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; +} diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java index 382f4eafb9..b8e07d1494 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java @@ -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; } diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/CapabilityDefinition.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/CapabilityDefinition.java new file mode 100644 index 0000000000..aafb583042 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/CapabilityDefinition.java @@ -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(); +} diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java index f2fee0b0c7..a4ba520872 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java @@ -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 path = splitPath(req); RestCollection rc = members.get(); - checkAccessAnnotations(rc.getClass()); + checkAccessAnnotations(null, rc.getClass()); RestResource rsrc = TopLevelResource.INSTANCE; - RestView 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 ac = (AcceptsPost) 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 ac = (AcceptsCreate) 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 c = - (RestCollection) view; + (RestCollection) 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 ac = (AcceptsPost) 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 ac = (AcceptsCreate) 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 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 m = - (RestModifyView) view; + (RestModifyView) viewData.view; inputRequestBody = parseRequest(req, inputType(m)); result = m.apply(rsrc, inputRequestBody); - } else if (view instanceof RestReadView) { - result = ((RestReadView) view).apply(rsrc); + } else if (viewData.view instanceof RestReadView) { + result = ((RestReadView) viewData.view).apply(rsrc); } else { throw new ResourceNotFoundException(); } @@ -766,7 +767,7 @@ public class RestApiServlet extends HttpServlet { return gz.setContentType(src.getContentType()); } - private RestView view( + private ViewData view( RestCollection rc, String method, List path) throws ResourceNotFoundException, MethodNotAllowedException, AmbiguousViewException { @@ -786,7 +787,7 @@ public class RestApiServlet extends HttpServlet { RestView 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 core = views.get("gerrit", name); if (core != null) { - return core; + return new ViewData(null, core); } Map> r = Maps.newTreeMap(); @@ -806,7 +807,9 @@ public class RestApiServlet extends HttpServlet { } if (r.size() == 1) { - return Iterables.getFirst(r.values(), null); + Map.Entry> 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 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 view; + + ViewData(String pluginName, RestView view) { + this.pluginName = pluginName; + this.view = view; + } + } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java index 54f1980718..aae6cad430 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java @@ -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 { private Set query; private final Provider self; + private final DynamicMap pluginCapabilities; @Inject - GetCapabilities(Provider self) { + GetCapabilities(Provider self, + DynamicMap pluginCapabilities) { this.self = self; + this.pluginCapabilities = pluginCapabilities; } @Override @@ -93,6 +98,14 @@ class GetCapabilities implements RestReadView { } } } + 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()); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java index 7ccb33f9d2..31cc4d2a4e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java @@ -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>() {}); 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); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCapabilities.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCapabilities.java index d92dfa7352..8062087aa4 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCapabilities.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCapabilities.java @@ -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 { + private final DynamicMap pluginCapabilities; + + @Inject + public ListCapabilities(DynamicMap pluginCapabilities) { + this.pluginCapabilities = pluginCapabilities; + } + @Override public Map apply(ConfigResource resource) throws AuthException, BadRequestException, ResourceConflictException, IllegalArgumentException, SecurityException, IllegalAccessException, NoSuchFieldException { Map output = Maps.newTreeMap(); + collectCoreCapabilities(output); + collectPluginCapabilities(output); + return output; + } + + private void collectCoreCapabilities(Map output) + throws IllegalAccessException, NoSuchFieldException { Class bundleClass = CapabilityConstants.get().getClass(); CapabilityConstants c = CapabilityConstants.get(); @@ -38,7 +56,18 @@ public class ListCapabilities implements RestReadView { String name = (String) bundleClass.getField(id).get(c); output.put(id, new CapabilityInfo(id, name)); } - return output; + } + + private void collectPluginCapabilities(Map output) { + for (String pluginName : pluginCapabilities.plugins()) { + for (Map.Entry> 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 { diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java index b6d50ee31d..1bca8028fa 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java @@ -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); } } diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/ListCapabilitiesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/ListCapabilitiesTest.java index f97ca5539b..992502fe2a 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/config/ListCapabilitiesTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/ListCapabilitiesTest.java @@ -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 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); } } diff --git a/gerrit-sshd/BUCK b/gerrit-sshd/BUCK index 93a3ef74e9..7a8610d5d2 100644 --- a/gerrit-sshd/BUCK +++ b/gerrit-sshd/BUCK @@ -16,6 +16,7 @@ java_library2( '//lib:guava', '//lib:gwtorm', '//lib:jsch', + '//lib:jsr305', '//lib/commons:codec', '//lib/guice:guice', '//lib/guice:guice-assistedinject', diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java index 98b740c0ce..0cf48d81dc 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java @@ -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 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> task; @@ -119,6 +127,11 @@ public abstract class BaseCommand implements Command { this.exit = callback; } + @Nullable + String getPluginName() { + return pluginName; + } + String getName() { return commandName; } diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java index 455b732cc7..0fa5d25617 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java @@ -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 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); } }