Introduce pull-replication user and internal group

Allows to identify the internal user created by the pull-replication
plugin as member of the pullreplication/internal-user and
Anonymous-Users groups.

Use the new pullreplication/internal-user impersonification when
receiving calls using bearer-token authentication, so that all
internal operations would benefit from the new custom identity.

The rationale behind this new internal user definition is
in the implementation of Gerrit's InternalUser, which does not
allow to define any membership and therefore won't be effectively
useful for accessing all the resources that the pull-replication
plugin needs:
- Access to the repositories
- Access to the git-upload-pack

The access to all refs of all repositories is achieved by
subclassing the InternalUser whilst the access to git-upload-pack
is automatically available through the membership to
Anonymous-Users.

Also, should the Gerrit admin decide to restrict who
can run the git-upload-pack, it would be possible to use the
new pullreplication/internal-user group.

Change-Id: I9c03693a3969506b0dbe6acbfba5de8ca8a1b020
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationModule.java
index 93bbde0..1671410 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationModule.java
@@ -44,6 +44,7 @@
 import com.googlesource.gerrit.plugins.replication.StartReplicationCapability;
 import com.googlesource.gerrit.plugins.replication.pull.api.FetchJob;
 import com.googlesource.gerrit.plugins.replication.pull.api.PullReplicationApiModule;
+import com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationGroupModule;
 import com.googlesource.gerrit.plugins.replication.pull.client.FetchApiClient;
 import com.googlesource.gerrit.plugins.replication.pull.client.FetchRestApiClient;
 import com.googlesource.gerrit.plugins.replication.pull.client.HttpClient;
@@ -73,6 +74,7 @@
   @Override
   protected void configure() {
 
+    install(new PullReplicationGroupModule());
     bind(BearerTokenProvider.class).in(Scopes.SINGLETON);
     bind(RevisionReader.class).in(Scopes.SINGLETON);
     bind(ApplyObject.class);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilter.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilter.java
index cbe1ab8..f196dbe 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilter.java
@@ -19,12 +19,12 @@
 import com.google.gerrit.httpd.AllRequestFilter;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.server.AccessPath;
-import com.google.gerrit.server.PluginUser;
 import com.google.gerrit.server.util.ManualRequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.name.Named;
+import com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationInternalUser;
 import java.io.IOException;
 import java.util.Optional;
 import java.util.regex.Matcher;
@@ -46,7 +46,7 @@
   private static final String BEARER_TOKEN = "BearerToken";
   private final DynamicItem<WebSession> session;
   private final String pluginName;
-  private final Provider<PluginUser> pluginUserProvider;
+  private final PullReplicationInternalUser pluginUser;
   private final Provider<ThreadLocalRequestContext> threadLocalRequestContext;
   private final String bearerToken;
   private final Pattern bearerTokenRegex = Pattern.compile("^Bearer\\s(.+)$");
@@ -55,12 +55,12 @@
   BearerAuthenticationFilter(
       DynamicItem<WebSession> session,
       @PluginName String pluginName,
-      Provider<PluginUser> pluginUserProvider,
+      PullReplicationInternalUser pluginUser,
       Provider<ThreadLocalRequestContext> threadLocalRequestContext,
       @Named(BEARER_TOKEN) String bearerToken) {
     this.session = session;
     this.pluginName = pluginName;
-    this.pluginUserProvider = pluginUserProvider;
+    this.pluginUser = pluginUser;
     this.threadLocalRequestContext = threadLocalRequestContext;
     this.bearerToken = bearerToken;
   }
@@ -88,7 +88,7 @@
 
       if (isBearerTokenAuthenticated(authorizationHeader, bearerToken))
         try (ManualRequestContext ctx =
-            new ManualRequestContext(pluginUserProvider.get(), threadLocalRequestContext.get())) {
+            new ManualRequestContext(pluginUser, threadLocalRequestContext.get())) {
           WebSession ws = session.get();
           ws.setAccessPathOk(AccessPath.REST_API, true);
           filterChain.doFilter(servletRequest, servletResponse);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationGroupBackend.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationGroupBackend.java
new file mode 100644
index 0000000..9521004
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationGroupBackend.java
@@ -0,0 +1,98 @@
+// Copyright (C) 2022 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.auth;
+
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
+import com.google.gerrit.entities.GroupReference;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AbstractGroupBackend;
+import com.google.gerrit.server.account.GroupMembership;
+import com.google.gerrit.server.account.ListGroupMembership;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.Arrays;
+import java.util.Collection;
+
+/** Backend to expose the pull-replication internal user group membership. */
+@Singleton
+class PullReplicationGroupBackend extends AbstractGroupBackend {
+  public static final AccountGroup.UUID INTERNAL_GROUP_UUID =
+      AccountGroup.uuid("pullreplication:internal-user");
+  public static final String INTERNAL_GROUP_NAME = "Pull-replication Internal User";
+  public static final String NAME_PREFIX = "pullreplication/";
+  public static final GroupDescription.Basic INTERNAL_GROUP_DESCRIPTION =
+      new GroupDescription.Basic() {
+
+        @Override
+        public String getUrl() {
+          return null;
+        }
+
+        @Override
+        public String getName() {
+          return INTERNAL_GROUP_NAME;
+        }
+
+        @Override
+        public AccountGroup.UUID getGroupUUID() {
+          return INTERNAL_GROUP_UUID;
+        }
+
+        @Override
+        public String getEmailAddress() {
+          return null;
+        }
+      };
+  private final PullReplicationInternalUser internalUser;
+
+  static final ListGroupMembership INTERNAL_GROUP_MEMBERSHIP =
+      new ListGroupMembership(
+          Arrays.asList(INTERNAL_GROUP_UUID, SystemGroupBackend.ANONYMOUS_USERS));
+
+  @Inject
+  public PullReplicationGroupBackend(PullReplicationInternalUser internalUser) {
+    this.internalUser = internalUser;
+  }
+
+  @Override
+  public boolean handles(AccountGroup.UUID uuid) {
+    return INTERNAL_GROUP_UUID.equals(uuid);
+  }
+
+  @Override
+  public GroupDescription.Basic get(AccountGroup.UUID uuid) {
+    return handles(uuid) ? INTERNAL_GROUP_DESCRIPTION : null;
+  }
+
+  @Override
+  public Collection<GroupReference> suggest(String name, ProjectState project) {
+    return Arrays.asList(
+        NAME_PREFIX.contains(name.toLowerCase())
+            ? GroupReference.create(INTERNAL_GROUP_UUID, INTERNAL_GROUP_NAME)
+            : GroupReference.create(name));
+  }
+
+  @Override
+  public GroupMembership membershipsOf(CurrentUser user) {
+    if (user.equals(internalUser)) {
+      return INTERNAL_GROUP_MEMBERSHIP;
+    }
+
+    return ListGroupMembership.EMPTY;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationGroupModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationGroupModule.java
new file mode 100644
index 0000000..583ba8e
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationGroupModule.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2022 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.auth;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.inject.AbstractModule;
+
+public class PullReplicationGroupModule extends AbstractModule {
+  @Override
+  protected void configure() {
+    DynamicSet.bind(binder(), GroupBackend.class).to(PullReplicationGroupBackend.class);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationInternalUser.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationInternalUser.java
new file mode 100644
index 0000000..e8b7666
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationInternalUser.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2022 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.auth;
+
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.PluginUser;
+import com.google.gerrit.server.account.GroupMembership;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class PullReplicationInternalUser extends PluginUser {
+
+  @Inject
+  protected PullReplicationInternalUser(@PluginName String pluginName) {
+    super(pluginName);
+  }
+
+  @Override
+  public GroupMembership getEffectiveGroups() {
+    return PullReplicationGroupBackend.INTERNAL_GROUP_MEMBERSHIP;
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilterTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilterTest.java
index bbbe66f..96c83a1 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilterTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilterTest.java
@@ -21,9 +21,9 @@
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.server.AccessPath;
-import com.google.gerrit.server.PluginUser;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.inject.Provider;
+import com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationInternalUser;
 import java.io.IOException;
 import javax.servlet.FilterChain;
 import javax.servlet.ServletException;
@@ -39,9 +39,8 @@
 
   @Mock private DynamicItem<WebSession> session;
   @Mock private WebSession webSession;
-  @Mock private Provider<PluginUser> pluginUserProvider;
   @Mock private Provider<ThreadLocalRequestContext> threadLocalRequestContextProvider;
-  @Mock private PluginUser pluginUser;
+  @Mock private PullReplicationInternalUser pluginUser;
   @Mock private ThreadLocalRequestContext threadLocalRequestContext;
   @Mock private HttpServletRequest httpServletRequest;
   @Mock private HttpServletResponse httpServletResponse;
@@ -53,21 +52,15 @@
     when(httpServletRequest.getRequestURI()).thenReturn(uri);
     when(httpServletRequest.getHeader("Authorization"))
         .thenReturn(String.format("Bearer %s", bearerToken));
-    when(pluginUserProvider.get()).thenReturn(pluginUser);
     when(threadLocalRequestContextProvider.get()).thenReturn(threadLocalRequestContext);
     when(session.get()).thenReturn(webSession);
     final BearerAuthenticationFilter filter =
         new BearerAuthenticationFilter(
-            session,
-            pluginName,
-            pluginUserProvider,
-            threadLocalRequestContextProvider,
-            bearerToken);
+            session, pluginName, pluginUser, threadLocalRequestContextProvider, bearerToken);
     filter.doFilter(httpServletRequest, httpServletResponse, filterChain);
 
     verify(httpServletRequest).getRequestURI();
     verify(httpServletRequest).getHeader("Authorization");
-    verify(pluginUserProvider).get();
     verify(threadLocalRequestContextProvider).get();
     verify(session).get();
     verify(webSession).setAccessPathOk(AccessPath.REST_API, true);
@@ -119,7 +112,7 @@
         new BearerAuthenticationFilter(
             session,
             pluginName,
-            pluginUserProvider,
+            pluginUser,
             threadLocalRequestContextProvider,
             "some-bearer-token");
     filter.doFilter(httpServletRequest, httpServletResponse, filterChain);
@@ -138,7 +131,7 @@
         new BearerAuthenticationFilter(
             session,
             pluginName,
-            pluginUserProvider,
+            pluginUser,
             threadLocalRequestContextProvider,
             "some-bearer-token");
     filter.doFilter(httpServletRequest, httpServletResponse, filterChain);
@@ -156,7 +149,7 @@
         new BearerAuthenticationFilter(
             session,
             pluginName,
-            pluginUserProvider,
+            pluginUser,
             threadLocalRequestContextProvider,
             "some-bearer-token");
     filter.doFilter(httpServletRequest, httpServletResponse, filterChain);
@@ -173,7 +166,7 @@
         new BearerAuthenticationFilter(
             session,
             pluginName,
-            pluginUserProvider,
+            pluginUser,
             threadLocalRequestContextProvider,
             "some-bearer-token");
     filter.doFilter(httpServletRequest, httpServletResponse, filterChain);
@@ -192,7 +185,7 @@
         new BearerAuthenticationFilter(
             session,
             pluginName,
-            pluginUserProvider,
+            pluginUser,
             threadLocalRequestContextProvider,
             "some-bearer-token");
     filter.doFilter(httpServletRequest, httpServletResponse, filterChain);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationGroupBackendIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationGroupBackendIT.java
new file mode 100644
index 0000000..87738e3
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationGroupBackendIT.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2022 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.auth;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationGroupBackend.INTERNAL_GROUP_DESCRIPTION;
+import static com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationGroupBackend.INTERNAL_GROUP_NAME;
+import static com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationGroupBackend.INTERNAL_GROUP_UUID;
+import static com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationGroupBackend.NAME_PREFIX;
+
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.SkipProjectClone;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.entities.GroupDescription;
+import com.google.gerrit.entities.GroupReference;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.GroupMembership;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import java.util.Collection;
+import org.junit.Test;
+
+@SkipProjectClone
+@TestPlugin(
+    name = "pull-replication",
+    sysModule = "com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationGroupModule")
+public class PullReplicationGroupBackendIT extends LightweightPluginDaemonTest {
+
+  @Test
+  public void shouldResolvePullReplicationInternalGroup() {
+    GroupDescription.Basic group = groupBackend.get(INTERNAL_GROUP_UUID);
+
+    assertThat(group).isNotNull();
+    assertThat(group).isEqualTo(INTERNAL_GROUP_DESCRIPTION);
+  }
+
+  @Test
+  public void shouldSuggestPullReplicationInternalGroup() {
+    Collection<GroupReference> groups = groupBackend.suggest(NAME_PREFIX, null);
+
+    assertThat(groups).isNotNull();
+    assertThat(groups).hasSize(1);
+
+    GroupReference groupReference = groups.iterator().next();
+    assertThat(groupReference.getName()).isEqualTo(INTERNAL_GROUP_NAME);
+    assertThat(groupReference.getUUID()).isEqualTo(INTERNAL_GROUP_UUID);
+  }
+
+  @Test
+  public void pullReplicationInternalUserShouldHaveMembershipOfInternalGroupAndAnonymousUsers() {
+    assertMemberOfInternalAndAnonymousUsers(
+        groupBackend.membershipsOf(getPullReplicationInternalUser()));
+  }
+
+  @Test
+  public void pullReplicationInternalUserShouldHaveEffectiveGroups() {
+    assertMemberOfInternalAndAnonymousUsers(getPullReplicationInternalUser().getEffectiveGroups());
+  }
+
+  private CurrentUser getPullReplicationInternalUser() {
+    CurrentUser user = plugin.getSysInjector().getInstance(PullReplicationInternalUser.class);
+    return user;
+  }
+
+  private void assertMemberOfInternalAndAnonymousUsers(GroupMembership userMembership) {
+    assertThat(userMembership.contains(INTERNAL_GROUP_UUID)).isTrue();
+    assertThat(userMembership.contains(SystemGroupBackend.ANONYMOUS_USERS)).isTrue();
+  }
+}