Add capability that allows to call the CheckCodeOwner REST endpoint

Having this capability host administrators can delegate the
investigation of code owner configurations to a trusted set of users.

Assigning this capability allows users to inspect code ownerships. This
may reveal accounts and secondary emails to the user that the user
cannot see otherwise. Hence this capability should only ge granted to
trusted users.

Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: Ic61022af9345988546d69c6efb3511c30efccb25
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
index 1bc41d7..86adb2b 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
@@ -37,7 +37,6 @@
 import com.google.gerrit.plugins.codeowners.backend.PathCodeOwnersResult;
 import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.BranchResource;
@@ -61,6 +60,7 @@
  * /projects/<project-name>/branches/<branch-name>/code_owners.check} requests.
  */
 public class CheckCodeOwner implements RestReadView<BranchResource> {
+  private final CheckCodeOwnerCapability checkCodeOwnerCapability;
   private final PermissionBackend permissionBackend;
   private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
   private final CodeOwnerConfigHierarchy codeOwnerConfigHierarchy;
@@ -76,6 +76,7 @@
 
   @Inject
   public CheckCodeOwner(
+      CheckCodeOwnerCapability checkCodeOwnerCapability,
       PermissionBackend permissionBackend,
       CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
       CodeOwnerConfigHierarchy codeOwnerConfigHierarchy,
@@ -83,6 +84,7 @@
       Provider<CodeOwnerResolver> codeOwnerResolverProvider,
       CodeOwners codeOwners,
       AccountsCollection accountsCollection) {
+    this.checkCodeOwnerCapability = checkCodeOwnerCapability;
     this.permissionBackend = permissionBackend;
     this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
     this.codeOwnerConfigHierarchy = codeOwnerConfigHierarchy;
@@ -115,7 +117,7 @@
   public Response<CodeOwnerCheckInfo> apply(BranchResource branchResource)
       throws BadRequestException, AuthException, IOException, ConfigInvalidException,
           PermissionBackendException {
-    permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
+    permissionBackend.currentUser().check(checkCodeOwnerCapability.getPermission());
 
     validateInput();
 
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerCapability.java b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerCapability.java
new file mode 100644
index 0000000..a127815
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerCapability.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2021 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.plugins.codeowners.restapi;
+
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.api.access.PluginPermission;
+import com.google.gerrit.extensions.config.CapabilityDefinition;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+/** Global capability that allows a user to call the {@link CheckCodeOwner} REST endpoint. */
+@Singleton
+public class CheckCodeOwnerCapability extends CapabilityDefinition {
+  public static final String ID = "checkCodeOwner";
+
+  private final String pluginName;
+
+  @Inject
+  CheckCodeOwnerCapability(@PluginName String pluginName) {
+    this.pluginName = pluginName;
+  }
+
+  @Override
+  public String getDescription() {
+    return "Check Code Owner";
+  }
+
+  public PluginPermission getPermission() {
+    return new PluginPermission(pluginName, ID);
+  }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/RestApiModule.java b/java/com/google/gerrit/plugins/codeowners/restapi/RestApiModule.java
index 68d057e..df4e6f7 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/RestApiModule.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/RestApiModule.java
@@ -19,6 +19,8 @@
 import static com.google.gerrit.server.project.BranchResource.BRANCH_KIND;
 import static com.google.gerrit.server.project.ProjectResource.PROJECT_KIND;
 
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.config.CapabilityDefinition;
 import com.google.gerrit.extensions.registration.DynamicMap;
 
 /** Guice module that binds the REST API for the code-owners plugin. */
@@ -32,6 +34,10 @@
     get(BRANCH_KIND, "code_owners.config_files").to(GetCodeOwnerConfigFiles.class);
     get(BRANCH_KIND, "code_owners.branch_config").to(GetCodeOwnerBranchConfig.class);
     post(BRANCH_KIND, "code_owners.rename").to(RenameEmail.class);
+
+    bind(CapabilityDefinition.class)
+        .annotatedWith(Exports.named(CheckCodeOwnerCapability.ID))
+        .to(CheckCodeOwnerCapability.class);
     get(BRANCH_KIND, "code_owners.check").to(CheckCodeOwner.class);
 
     factory(CodeOwnerJson.Factory.class);
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java
index ea4fc3a..fc5fddf 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java
@@ -16,7 +16,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
 import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerCheckInfoSubject.assertThat;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.gerrit.acceptance.TestAccount;
@@ -40,6 +42,7 @@
 import com.google.gerrit.plugins.codeowners.backend.findowners.FindOwnersBackend;
 import com.google.gerrit.plugins.codeowners.backend.proto.ProtoBackend;
 import com.google.gerrit.plugins.codeowners.config.BackendConfig;
+import com.google.gerrit.plugins.codeowners.restapi.CheckCodeOwnerCapability;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.externalids.ExternalId;
@@ -87,15 +90,33 @@
   }
 
   @Test
-  public void requiresCallerToBeAdmin() throws Exception {
+  public void requiresCallerToBeAdminOrHaveTheCheckCodeOwnerCapability() throws Exception {
     requestScopeOperations.setApiUser(user.id());
     AuthException authException =
         assertThrows(AuthException.class, () -> checkCodeOwner(ROOT_PATH, user.email()));
-    assertThat(authException).hasMessageThat().isEqualTo("administrate server not permitted");
+    assertThat(authException)
+        .hasMessageThat()
+        .isEqualTo(
+            String.format("%s for plugin code-owners not permitted", CheckCodeOwnerCapability.ID));
   }
 
   @Test
-  public void checkCodeOwner() throws Exception {
+  public void checkCodeOwner_byAdmin() throws Exception {
+    testCheckCodeOwner();
+  }
+
+  @Test
+  public void checkCodeOwner_byUserThatHasTheCheckCodeOwnerCapability() throws Exception {
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability("code-owners-" + CheckCodeOwnerCapability.ID).group(REGISTERED_USERS))
+        .update();
+
+    requestScopeOperations.setApiUser(user.id());
+    testCheckCodeOwner();
+  }
+
+  private void testCheckCodeOwner() throws Exception {
     TestAccount codeOwner =
         accountCreator.create(
             "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
diff --git a/resources/Documentation/rest-api.md b/resources/Documentation/rest-api.md
index 8902d36..c93adc0 100644
--- a/resources/Documentation/rest-api.md
+++ b/resources/Documentation/rest-api.md
@@ -232,8 +232,8 @@
 | `path`      | mandatory | Path for which the code ownership should be checked.
 | `user`      | optional  | User for which the code owner visibility should be checked. If not specified the code owner visibility is not checked. Can be used to investigate why a code owner is not shown/suggested to this user.
 
-Requires that the caller has the [Administrate
-Server](../../../Documentation/access-control.html#capability_administrateServer)
+Requires that the caller has the [Check Code Owner](#checkCodeOwner) or the
+[Administrate Server](../../../Documentation/access-control.html#capability_administrateServer)
 global capability.
 
 This REST endpoint is intended to investigate code owner configurations that do
@@ -841,6 +841,19 @@
 
 ---
 
+## <a id="capabilities">Capabilities
+
+### <a id="checkCodeOwner">Check Code Owner
+
+Global capability that allows a user to call the [Check Code
+Owner](#check-code-owner) REST endpoint.
+
+Assigning this capability allows users to inspect code ownerships. This may
+reveal accounts and secondary emails to the user that the user cannot see
+otherwise. Hence this capability should only ge granted to trusted users.
+
+Administrators have this capability implicitly assigned.
+
 Back to [@PLUGIN@ documentation index](index.html)
 
 Part of [Gerrit Code Review](../../../Documentation/index.html)