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

@@ -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;
}
}
}