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:
@@ -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
|
||||
-------------
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ java_library2(
|
||||
'//lib:guava',
|
||||
'//lib:gwtorm',
|
||||
'//lib:jsch',
|
||||
'//lib:jsr305',
|
||||
'//lib/commons:codec',
|
||||
'//lib/guice:guice',
|
||||
'//lib/guice:guice-assistedinject',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user