diff --git a/Documentation/rest-api.txt b/Documentation/rest-api.txt index b95d5e940d..f07b9a9e61 100644 --- a/Documentation/rest-api.txt +++ b/Documentation/rest-api.txt @@ -55,8 +55,53 @@ save on network transfer time for larger responses. Endpoints --------- -List Projects: /projects/ -~~~~~~~~~~~~~~~~~~~~~~~~~ +[[accounts_self_capabilities]] +/accounts/self/capabilities (Account Capabilities) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Returns the global capabilities (such as createProject or +createGroup) that are enabled for the calling user. This can be used +by UI tools to discover if administrative features are available +to the caller, so they can hide (or show) relevant UI actions. + +---- + GET /accounts/self/capabilities?format=JSON HTTP/1.0 + + )]}' + { + "queryLimit": { + "min": 0, + "max": 500 + } + } +---- + +Administrator that has authenticated with digest authentication: +---- + GET /a/accounts/self/capabilities?format=JSON HTTP/1.0 + Authorization: Digest username="admin", realm="Gerrit Code Review", nonce="... + + )]}' + { + "administrateServer": true, + "queryLimit": { + "min": 0, + "max": 500 + }, + "createAccount": true, + "createGroup": true, + "createProject": true, + "killTask": true, + "viewCaches": true, + "flushCaches": true, + "viewConnections": true, + "viewQueue": true, + "startReplication": true + } +---- + +[[projects]] +/projects/ (List Projects) +~~~~~~~~~~~~~~~~~~~~~~~~~~ Lists the projects accessible by the caller. This is the same as using the link:cmd-ls-projects.html[ls-projects] command over SSH, and accepts the same options as query parameters. diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java index d3d2a4d71f..64444b4dee 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java @@ -15,6 +15,8 @@ package com.google.gerrit.common.data; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; /** Server wide capabilities. Represented as {@link Permission} objects. */ @@ -73,23 +75,34 @@ public class GlobalCapability { /** Can view all pending tasks in the queue (not just the filtered set). */ public static final String VIEW_QUEUE = "viewQueue"; + private static final List NAMES_ALL; private static final List NAMES_LC; static { - NAMES_LC = new ArrayList(); - NAMES_LC.add(ADMINISTRATE_SERVER.toLowerCase()); - NAMES_LC.add(CREATE_ACCOUNT.toLowerCase()); - NAMES_LC.add(CREATE_GROUP.toLowerCase()); - NAMES_LC.add(CREATE_PROJECT.toLowerCase()); - NAMES_LC.add(EMAIL_REVIEWERS.toLowerCase()); - NAMES_LC.add(FLUSH_CACHES.toLowerCase()); - NAMES_LC.add(KILL_TASK.toLowerCase()); - NAMES_LC.add(PRIORITY.toLowerCase()); - NAMES_LC.add(QUERY_LIMIT.toLowerCase()); - NAMES_LC.add(START_REPLICATION.toLowerCase()); - NAMES_LC.add(VIEW_CACHES.toLowerCase()); - NAMES_LC.add(VIEW_CONNECTIONS.toLowerCase()); - NAMES_LC.add(VIEW_QUEUE.toLowerCase()); + NAMES_ALL = new ArrayList(); + NAMES_ALL.add(ADMINISTRATE_SERVER); + NAMES_ALL.add(CREATE_ACCOUNT); + NAMES_ALL.add(CREATE_GROUP); + NAMES_ALL.add(CREATE_PROJECT); + NAMES_ALL.add(EMAIL_REVIEWERS); + NAMES_ALL.add(FLUSH_CACHES); + NAMES_ALL.add(KILL_TASK); + NAMES_ALL.add(PRIORITY); + NAMES_ALL.add(QUERY_LIMIT); + NAMES_ALL.add(START_REPLICATION); + NAMES_ALL.add(VIEW_CACHES); + NAMES_ALL.add(VIEW_CONNECTIONS); + NAMES_ALL.add(VIEW_QUEUE); + + NAMES_LC = new ArrayList(NAMES_ALL.size()); + for (String name : NAMES_ALL) { + NAMES_LC.add(name.toLowerCase()); + } + } + + /** @return all valid capability names. */ + public static Collection getAllNames() { + return Collections.unmodifiableList(NAMES_ALL); } /** @return true if the name is recognized as a capability name. */ diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountCapabilities.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountCapabilities.java new file mode 100644 index 0000000000..0565d3e37a --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountCapabilities.java @@ -0,0 +1,36 @@ +// Copyright (C) 2012 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.client.account; + +import com.google.gerrit.client.rpc.RestApi; +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwtjsonrpc.common.AsyncCallback; + +/** Capabilities the caller has from {@code /accounts/self/capabilities}. */ +public class AccountCapabilities extends JavaScriptObject { + public static void all(AsyncCallback cb, String... filter) { + RestApi api = new RestApi("/accounts/self/capabilities"); + for (String name : filter) { + api.addParameter("q", name); + } + api.send(cb); + } + + protected AccountCapabilities() { + } + + public final native boolean canPerform(String name) + /*-{ return this[name] ? true : false; }-*/; +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java index 9d62d34baa..c0a55d0f34 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java @@ -14,8 +14,11 @@ package com.google.gerrit.client.admin; +import static com.google.gerrit.common.data.GlobalCapability.CREATE_GROUP; + import com.google.gerrit.client.Dispatcher; import com.google.gerrit.client.Gerrit; +import com.google.gerrit.client.account.AccountCapabilities; import com.google.gerrit.client.rpc.GerritCallback; import com.google.gerrit.client.rpc.ScreenLoadCallback; import com.google.gerrit.client.ui.AccountScreen; @@ -48,11 +51,17 @@ public class GroupListScreen extends AccountScreen { @Override protected void onLoad() { super.onLoad(); + addPanel.setVisible(false); + AccountCapabilities.all(new GerritCallback() { + @Override + public void onSuccess(AccountCapabilities ac) { + addPanel.setVisible(ac.canPerform(CREATE_GROUP)); + } + }, CREATE_GROUP); Util.GROUP_SVC .visibleGroups(new ScreenLoadCallback(this) { @Override protected void preDisplay(GroupList result) { - addPanel.setVisible(result.isCanCreateGroup()); groups.display(result.getGroups()); groups.finishDisplay(); } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java index 3599c3c032..35a86e8898 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java @@ -14,8 +14,12 @@ package com.google.gerrit.client.admin; +import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT; + import com.google.gerrit.client.Dispatcher; import com.google.gerrit.client.Gerrit; +import com.google.gerrit.client.account.AccountCapabilities; +import com.google.gerrit.client.rpc.GerritCallback; import com.google.gerrit.client.rpc.ScreenLoadCallback; import com.google.gerrit.client.ui.Hyperlink; import com.google.gerrit.client.ui.ProjectsTable; @@ -33,10 +37,16 @@ public class ProjectListScreen extends Screen { @Override protected void onLoad() { super.onLoad(); + createProjectLinkPanel.setVisible(false); + AccountCapabilities.all(new GerritCallback() { + @Override + public void onSuccess(AccountCapabilities ac) { + createProjectLinkPanel.setVisible(ac.canPerform(CREATE_PROJECT)); + } + }, CREATE_PROJECT); Util.PROJECT_SVC.visibleProjects(new ScreenLoadCallback(this) { @Override protected void preDisplay(final ProjectList result) { - createProjectLinkPanel.setVisible(result.canCreateProject()); projects.display(result.getProjects()); projects.finishDisplay(); } diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java index 77046e0c34..72e7bfed6c 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java @@ -23,6 +23,7 @@ import com.google.gerrit.httpd.raw.LegacyGerritServlet; import com.google.gerrit.httpd.raw.SshInfoServlet; import com.google.gerrit.httpd.raw.StaticServlet; import com.google.gerrit.httpd.raw.ToolServlet; +import com.google.gerrit.httpd.rpc.account.AccountCapabilitiesServlet; import com.google.gerrit.httpd.rpc.project.ListProjectsServlet; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Project; @@ -72,6 +73,7 @@ class UrlModule extends ServletModule { serveRegex("^/r/(.+)/?$").with(DirectChangeByCommit.class); filter("/a/*").through(RequireIdentifiedUserFilter.class); + serveRegex("^/(?:a/)?accounts/self/capabilities$").with(AccountCapabilitiesServlet.class); serveRegex("^/(a/)?projects/$").with(ListProjectsServlet.class); } diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountCapabilitiesServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountCapabilitiesServlet.java new file mode 100644 index 0000000000..0d0ffe7a95 --- /dev/null +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountCapabilitiesServlet.java @@ -0,0 +1,188 @@ +// Copyright (C) 2012 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.httpd.rpc.account; + +import static com.google.gerrit.common.data.GlobalCapability.CREATE_ACCOUNT; +import static com.google.gerrit.common.data.GlobalCapability.CREATE_GROUP; +import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT; +import static com.google.gerrit.common.data.GlobalCapability.FLUSH_CACHES; +import static com.google.gerrit.common.data.GlobalCapability.KILL_TASK; +import static com.google.gerrit.common.data.GlobalCapability.PRIORITY; +import static com.google.gerrit.common.data.GlobalCapability.START_REPLICATION; +import static com.google.gerrit.common.data.GlobalCapability.VIEW_CACHES; +import static com.google.gerrit.common.data.GlobalCapability.VIEW_CONNECTIONS; +import static com.google.gerrit.common.data.GlobalCapability.VIEW_QUEUE; + +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.httpd.RestApiServlet; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.OutputFormat; +import com.google.gerrit.server.account.CapabilityControl; +import com.google.gerrit.server.git.QueueProvider; +import com.google.gson.reflect.TypeToken; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; + +import org.kohsuke.args4j.Option; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@Singleton +public class AccountCapabilitiesServlet extends RestApiServlet { + private static final long serialVersionUID = 1L; + private final ParameterParser paramParser; + private final Provider factory; + + @Inject + AccountCapabilitiesServlet( + ParameterParser paramParser, Provider factory) { + this.paramParser = paramParser; + this.factory = factory; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse res) + throws IOException { + Impl impl = factory.get(); + if (acceptsJson(req)) { + impl.format = OutputFormat.JSON_COMPACT; + } + if (paramParser.parse(impl, req, res)) { + impl.compute(); + + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + OutputStreamWriter out = new OutputStreamWriter(buf, "UTF-8"); + if (impl.format.isJson()) { + res.setContentType(JSON_TYPE); + buf.write(JSON_MAGIC); + impl.format.newGson().toJson( + impl.have, + new TypeToken>() {}.getType(), + out); + out.flush(); + buf.write('\n'); + } else { + res.setContentType("text/plain"); + for (Map.Entry e : impl.have.entrySet()) { + out.write(e.getKey()); + if (!(e.getValue() instanceof Boolean)) { + out.write(": "); + out.write(e.getValue().toString()); + } + out.write('\n'); + } + out.flush(); + } + res.setCharacterEncoding("UTF-8"); + send(req, res, buf.toByteArray()); + } + } + + static class Impl { + private final CapabilityControl cc; + private final Map have; + + @Option(name = "--format", metaVar = "FMT", usage = "Output display format") + private OutputFormat format = OutputFormat.TEXT; + + @Option(name = "-q", metaVar = "CAP", multiValued = true, usage = "Capability to inspect") + void addQuery(String name) { + if (query == null) { + query = Sets.newHashSet(); + } + query.add(name.toLowerCase()); + } + private Set query; + + @Inject + Impl(CurrentUser user) { + cc = user.getCapabilities(); + have = Maps.newLinkedHashMap(); + } + + void compute() { + for (String name : GlobalCapability.getAllNames()) { + if (!name.equals(PRIORITY) && want(name) && cc.canPerform(name)) { + if (GlobalCapability.hasRange(name)) { + have.put(name, new Range(cc.getRange(name))); + } else { + have.put(name, true); + } + } + } + + have.put(CREATE_ACCOUNT, cc.canCreateAccount()); + have.put(CREATE_GROUP, cc.canCreateGroup()); + have.put(CREATE_PROJECT, cc.canCreateProject()); + have.put(KILL_TASK, cc.canKillTask()); + have.put(VIEW_CACHES, cc.canViewCaches()); + have.put(FLUSH_CACHES, cc.canFlushCaches()); + have.put(VIEW_CONNECTIONS, cc.canViewConnections()); + have.put(VIEW_QUEUE, cc.canViewQueue()); + have.put(START_REPLICATION, cc.canStartReplication()); + + QueueProvider.QueueType queue = cc.getQueueType(); + if (queue != QueueProvider.QueueType.INTERACTIVE + || (query != null && query.contains(PRIORITY))) { + have.put(PRIORITY, queue); + } + + Iterator> itr = have.entrySet().iterator(); + while (itr.hasNext()) { + Map.Entry e = itr.next(); + if (!want(e.getKey())) { + itr.remove(); + } else if (e.getValue() instanceof Boolean && !((Boolean) e.getValue())) { + itr.remove(); + } + } + } + + private boolean want(String name) { + return query == null || query.contains(name.toLowerCase()); + } + } + + private static class Range { + private transient PermissionRange range; + @SuppressWarnings("unused") + private int min; + @SuppressWarnings("unused") + private int max; + + Range(PermissionRange r) { + range = r; + min = r.getMin(); + max = r.getMax(); + } + + @Override + public String toString() { + return range.toString(); + } + } +}