Add modify reviewers config plugin capability

Allow to use plugin own capability in addition to project owners ACL.

Feature: Issue 9224
Change-Id: I2e214cb39b08788f4f2fff03d82ab8811960d3ca
diff --git a/src/main/java/com/googlesource/gerrit/plugins/reviewers/ModifyReviewersConfigCapability.java b/src/main/java/com/googlesource/gerrit/plugins/reviewers/ModifyReviewersConfigCapability.java
new file mode 100644
index 0000000..4c6cb9a
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/reviewers/ModifyReviewersConfigCapability.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2018 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.googlesource.gerrit.plugins.reviewers;
+
+import com.google.gerrit.extensions.config.CapabilityDefinition;
+
+public class ModifyReviewersConfigCapability extends CapabilityDefinition {
+  static final String MODIFY_REVIEWERS_CONFIG = "modifyReviewersConfig";
+
+  @Override
+  public String getDescription() {
+    return "Modify Reviewers Config";
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/reviewers/Module.java b/src/main/java/com/googlesource/gerrit/plugins/reviewers/Module.java
index 3e949d6..9951538 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/reviewers/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/reviewers/Module.java
@@ -15,8 +15,10 @@
 package com.googlesource.gerrit.plugins.reviewers;
 
 import static com.google.gerrit.server.project.ProjectResource.PROJECT_KIND;
+import static com.googlesource.gerrit.plugins.reviewers.ModifyReviewersConfigCapability.MODIFY_REVIEWERS_CONFIG;
 
 import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.config.CapabilityDefinition;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.extensions.events.RevisionCreatedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
@@ -48,6 +50,10 @@
 
   @Override
   protected void configure() {
+    bind(CapabilityDefinition.class)
+        .annotatedWith(Exports.named(MODIFY_REVIEWERS_CONFIG))
+        .to(ModifyReviewersConfigCapability.class);
+
     if (enableUI) {
       DynamicSet.bind(binder(), TopMenu.class).to(ReviewersTopMenu.class);
       DynamicSet.bind(binder(), WebUiPlugin.class).toInstance(new GwtPlugin("reviewers"));
diff --git a/src/main/java/com/googlesource/gerrit/plugins/reviewers/PutReviewers.java b/src/main/java/com/googlesource/gerrit/plugins/reviewers/PutReviewers.java
index f23e275..b3de693 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/reviewers/PutReviewers.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/reviewers/PutReviewers.java
@@ -14,7 +14,11 @@
 
 package com.googlesource.gerrit.plugins.reviewers;
 
+import static com.googlesource.gerrit.plugins.reviewers.ModifyReviewersConfigCapability.MODIFY_REVIEWERS_CONFIG;
+
 import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.api.access.PluginPermission;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -25,6 +29,7 @@
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.group.GroupsCollection;
+import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gwtorm.server.OrmException;
@@ -55,6 +60,7 @@
   private final ProjectCache projectCache;
   private final AccountResolver accountResolver;
   private final Provider<GroupsCollection> groupsCollection;
+  private final PermissionBackend permissionBackend;
 
   @Inject
   PutReviewers(
@@ -63,13 +69,15 @@
       Provider<MetaDataUpdate.User> metaDataUpdateFactory,
       ProjectCache projectCache,
       AccountResolver accountResolver,
-      Provider<GroupsCollection> groupsCollection) {
+      Provider<GroupsCollection> groupsCollection,
+      PermissionBackend permissionBackend) {
     this.pluginName = pluginName;
     this.config = config;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.projectCache = projectCache;
     this.accountResolver = accountResolver;
     this.groupsCollection = groupsCollection;
+    this.permissionBackend = permissionBackend;
   }
 
   @Override
@@ -77,10 +85,16 @@
       throws RestApiException {
     Project.NameKey projectName = rsrc.getNameKey();
     ReviewersConfig.ForProject cfg = config.forProject(projectName);
-    if (!rsrc.getControl().isOwner() || cfg == null) {
+    if (cfg == null) {
       throw new ResourceNotFoundException("Project" + projectName.get() + " not found");
     }
 
+    PermissionBackend.WithUser userPermission = permissionBackend.user(rsrc.getUser());
+    if (!rsrc.getControl().isOwner()
+        && !userPermission.testOrFalse(new PluginPermission(pluginName, MODIFY_REVIEWERS_CONFIG))) {
+      throw new AuthException("not allowed to modify reviewers config");
+    }
+
     try (MetaDataUpdate md = metaDataUpdateFactory.get().create(projectName)) {
       if (input.action == Action.ADD) {
         validateReviewer(input.reviewer);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/reviewers/client/AccountCapabilities.java b/src/main/java/com/googlesource/gerrit/plugins/reviewers/client/AccountCapabilities.java
new file mode 100644
index 0000000..397ffcb
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/reviewers/client/AccountCapabilities.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2018 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.googlesource.gerrit.plugins.reviewers.client;
+
+import com.google.gerrit.plugin.client.rpc.RestApi;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+public class AccountCapabilities extends JavaScriptObject {
+  static String MODIFY_REVIEWERS_CONFIG = "reviewers-modifyReviewersConfig";
+
+  public static void queryPluginCapability(AsyncCallback<AccountCapabilities> cb) {
+    new RestApi("/accounts/self/capabilities").addParameter("q", MODIFY_REVIEWERS_CONFIG).get(cb);
+  }
+
+  protected AccountCapabilities() {}
+
+  public final native boolean canPerform(String name) /*-{ return this[name] ? true : false; }-*/;
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/reviewers/client/ReviewersScreen.java b/src/main/java/com/googlesource/gerrit/plugins/reviewers/client/ReviewersScreen.java
index ba601ae..47bedd7 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/reviewers/client/ReviewersScreen.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/reviewers/client/ReviewersScreen.java
@@ -14,6 +14,8 @@
 
 package com.googlesource.gerrit.plugins.reviewers.client;
 
+import static com.googlesource.gerrit.plugins.reviewers.client.AccountCapabilities.MODIFY_REVIEWERS_CONFIG;
+
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.plugin.client.rpc.RestApi;
@@ -48,10 +50,11 @@
   }
 
   private boolean isOwner;
+  private boolean hasModifyReviewersConfigCapability;
   private String projectName;
   private Set<ReviewerEntry> rEntries;
 
-  ReviewersScreen(final String projectName) {
+  ReviewersScreen(String projectName) {
     setStyleName("reviewers-panel");
     this.projectName = projectName;
     this.rEntries = new HashSet<>();
@@ -64,7 +67,24 @@
               @Override
               public void onSuccess(NativeMap<ProjectAccessInfo> result) {
                 isOwner = result.get(projectName).isOwner();
-                display();
+                if (isOwner) {
+                  display();
+                } else {
+                  // TODO(davido): Find a way to run above and below requests in parallel
+                  AccountCapabilities.queryPluginCapability(
+                      new AsyncCallback<AccountCapabilities>() {
+
+                        @Override
+                        public void onSuccess(AccountCapabilities result) {
+                          hasModifyReviewersConfigCapability =
+                              result.canPerform(MODIFY_REVIEWERS_CONFIG);
+                          display();
+                        }
+
+                        @Override
+                        public void onFailure(Throwable caught) {}
+                      });
+                }
               }
 
               @Override
@@ -124,7 +144,7 @@
             doSave(Action.REMOVE, e);
           }
         });
-    removeButton.setVisible(isOwner);
+    removeButton.setVisible(isModifiable());
 
     HorizontalPanel p = new HorizontalPanel();
     p.add(l);
@@ -161,9 +181,9 @@
             reviewerBox.setText("");
           }
         });
-    filterBox.setEnabled(isOwner);
-    reviewerBox.setEnabled(isOwner);
-    addButton.setEnabled(isOwner);
+    filterBox.setEnabled(isModifiable());
+    reviewerBox.setEnabled(isModifiable());
+    addButton.setEnabled(isModifiable());
 
     Panel p = new VerticalPanel();
     p.setStyleName("reviewers-inputPanel");
@@ -172,6 +192,10 @@
     return p;
   }
 
+  boolean isModifiable() {
+    return isOwner || hasModifyReviewersConfigCapability;
+  }
+
   void doSave(Action action, ReviewerEntry entry) {
     ChangeReviewersInput in = ChangeReviewersInput.create();
     in.setAction(action);
diff --git a/src/main/resources/Documentation/rest-api.md b/src/main/resources/Documentation/rest-api.md
index 6fe3733..ebd04d4 100644
--- a/src/main/resources/Documentation/rest-api.md
+++ b/src/main/resources/Documentation/rest-api.md
@@ -56,6 +56,9 @@
 The change to reviewers must be provided in the request body inside
 a [ConfigReviewersInput](#config-reviewers-input) entity.
 
+Caller must be a member of a group that is granted the 'Modify Reviewers Config'
+capability (provided by this plugin) or be a Project Owner for the project.
+
 #### Request
 
 ```