Add 'Pull Replication' capability

Add possibility to restrict call to Fetch REST endpoint
for triggering the pull replication.

Feature: Issue 12560
Change-Id: Id79069d6a7fd03fdcba1fdb68ebc6cb326394f4b
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchAction.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchAction.java
index d51590d..c44bcbc 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchAction.java
@@ -20,6 +20,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -40,13 +41,18 @@
   private final FetchCommand command;
   private final WorkQueue workQueue;
   private final DynamicItem<UrlFormatter> urlFormatter;
+  private final FetchPreconditions preConditions;
 
   @Inject
   public FetchAction(
-      FetchCommand command, WorkQueue workQueue, DynamicItem<UrlFormatter> urlFormatter) {
+      FetchCommand command,
+      WorkQueue workQueue,
+      DynamicItem<UrlFormatter> urlFormatter,
+      FetchPreconditions preConditions) {
     this.command = command;
     this.workQueue = workQueue;
     this.urlFormatter = urlFormatter;
+    this.preConditions = preConditions;
   }
 
   public static class Input {
@@ -57,6 +63,10 @@
 
   @Override
   public Response<?> apply(ProjectResource resource, Input input) throws RestApiException {
+
+    if (!preConditions.canCallFetchApi()) {
+      throw new AuthException("not allowed to call fetch command");
+    }
     try {
       if (Strings.isNullOrEmpty(input.label)) {
         throw new BadRequestException("Source label cannot be null or empty");
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchApiCapability.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchApiCapability.java
new file mode 100644
index 0000000..73a4ac5
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchApiCapability.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2020 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.replication.pull.api;
+
+import com.google.gerrit.extensions.config.CapabilityDefinition;
+
+public class FetchApiCapability extends CapabilityDefinition {
+  static final String CALL_FETCH_ACTION = "callFetchAction";
+
+  @Override
+  public String getDescription() {
+    return "Pull Replication";
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchPreconditions.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchPreconditions.java
new file mode 100644
index 0000000..ca1557a
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchPreconditions.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2020 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.replication.pull.api;
+
+import static com.googlesource.gerrit.plugins.replication.pull.api.FetchApiCapability.CALL_FETCH_ACTION;
+
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.api.access.PluginPermission;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class FetchPreconditions {
+  private final String pluginName;
+  private final PermissionBackend permissionBackend;
+  private final Provider<CurrentUser> userProvider;
+
+  @Inject
+  public FetchPreconditions(
+      @PluginName String pluginName,
+      Provider<CurrentUser> userProvider,
+      PermissionBackend permissionBackend) {
+    this.pluginName = pluginName;
+    this.userProvider = userProvider;
+    this.permissionBackend = permissionBackend;
+  }
+
+  public Boolean canCallFetchApi() {
+    PermissionBackend.WithUser userPermission = permissionBackend.user(userProvider.get());
+    return userPermission.testOrFalse(GlobalPermission.ADMINISTRATE_SERVER)
+        || userPermission.testOrFalse(new PluginPermission(pluginName, CALL_FETCH_ACTION));
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiModule.java
index 470c985..1663ad2 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiModule.java
@@ -15,7 +15,10 @@
 package com.googlesource.gerrit.plugins.replication.pull.api;
 
 import static com.google.gerrit.server.project.ProjectResource.PROJECT_KIND;
+import static com.googlesource.gerrit.plugins.replication.pull.api.FetchApiCapability.CALL_FETCH_ACTION;
 
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.config.CapabilityDefinition;
 import com.google.gerrit.extensions.restapi.RestApiModule;
 import com.google.inject.Scopes;
 
@@ -24,5 +27,10 @@
   protected void configure() {
     bind(FetchAction.class).in(Scopes.SINGLETON);
     post(PROJECT_KIND, "fetch").to(FetchAction.class);
+
+    bind(FetchPreconditions.class).in(Scopes.SINGLETON);
+    bind(CapabilityDefinition.class)
+        .annotatedWith(Exports.named(CALL_FETCH_ACTION))
+        .to(FetchApiCapability.class);
   }
 }
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index 46ec740..d921e0b 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -8,3 +8,10 @@
 configuration is not recommended.  It is also possible to specify a
 local path as replication source. This makes e.g. sense if a network
 share is mounted to which the repositories should be replicated from.
+
+Access
+------
+
+To be allowed to trigger pull replication a user must be a member of a
+group that is granted the 'Pull Replication' capability (provided
+by this plugin) or the 'Administrate Server' capability.
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionTest.java
index 39c6717..2c888c8 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionTest.java
@@ -18,11 +18,12 @@
 import static org.apache.http.HttpStatus.SC_ACCEPTED;
 import static org.apache.http.HttpStatus.SC_CREATED;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.anyString;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.when;
 
 import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -60,6 +61,7 @@
   @Mock DynamicItem<UrlFormatter> urlFormatterDynamicItem;
   @Mock UrlFormatter urlFormatter;
   @Mock WorkQueue.Task<Void> task;
+  @Mock FetchPreconditions preConditions;
 
   @Before
   public void setup() {
@@ -75,8 +77,9 @@
             });
     when(urlFormatterDynamicItem.get()).thenReturn(urlFormatter);
     when(task.getTaskId()).thenReturn(taskId);
+    when(preConditions.canCallFetchApi()).thenReturn(true);
 
-    fetchAction = new FetchAction(fetchCommand, workQueue, urlFormatterDynamicItem);
+    fetchAction = new FetchAction(fetchCommand, workQueue, urlFormatterDynamicItem, preConditions);
   }
 
   @Test
@@ -205,6 +208,18 @@
     fetchAction.apply(projectResource, inputParams);
   }
 
+  @Test(expected = AuthException.class)
+  public void shouldThrowAuthExceptionWhenCallFetchActionCapabilityNotAssigned()
+      throws RestApiException {
+    FetchAction.Input inputParams = new FetchAction.Input();
+    inputParams.label = label;
+    inputParams.refName = refName;
+
+    when(preConditions.canCallFetchApi()).thenReturn(false);
+
+    fetchAction.apply(projectResource, inputParams);
+  }
+
   @Test
   public void shouldReturnScheduledTaskForAsyncCall() throws RestApiException {
     FetchAction.Input inputParams = new FetchAction.Input();