Replication Security

Replication now makes use of the standard security system in Gerrit. This is
done by tying to Gerrit groups via replication.config.
diff --git a/Documentation/config-replication.txt b/Documentation/config-replication.txt
index 2017ed7..0f767c7 100644
--- a/Documentation/config-replication.txt
+++ b/Documentation/config-replication.txt
@@ -45,6 +45,8 @@
     push = +refs/heads/*
     push = +refs/tags/*
     threads = 3
+    authGroup = Public Mirror Group
+    authGroup = Second Public Mirror Group
 ====
 
 To manually trigger replication at runtime, see
@@ -151,6 +153,18 @@
 +
 By default, 1 thread.
 
+[[remote.name.authGroup]]remote.<name>.authGroup:
++
+Specifies the name of a group that the remote should use to access
+the repositories. Multiple authGroups may be specified within a
+single remote block to signify a wider access right. In the project
+administration web interface the read access can be specified for
+this group to control if a project should be replicated or not to the
+remote.
++
+By default, replicates without group control, i.e replicates
+everything to all remotes.
+
 
 [[ssh_config]]File `~/.ssh/config`
 ----------------------------------
diff --git a/src/main/java/com/google/gerrit/git/PushReplication.java b/src/main/java/com/google/gerrit/git/PushReplication.java
index 18c2fc9..aea8b7c 100644
--- a/src/main/java/com/google/gerrit/git/PushReplication.java
+++ b/src/main/java/com/google/gerrit/git/PushReplication.java
@@ -14,8 +14,16 @@
 
 package com.google.gerrit.git;
 
+import com.google.gerrit.client.reviewdb.AccountGroup;
 import com.google.gerrit.client.reviewdb.Project;
+import com.google.gerrit.client.reviewdb.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.ReplicationUser;
 import com.google.gerrit.server.config.SitePath;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.SchemaFactory;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
@@ -47,9 +55,11 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
 /** Manages automatic replication to remote repositories. */
@@ -72,12 +82,18 @@
   private final Injector injector;
   private final WorkQueue workQueue;
   private final List<ReplicationConfig> configs;
+  private final SchemaFactory<ReviewDb> database;
+  private final ReplicationUser.Factory replicationUserFactory;
 
   @Inject
   PushReplication(final Injector i, final WorkQueue wq,
-      @SitePath final File sitePath) throws ConfigInvalidException, IOException {
+      @SitePath final File sitePath, final ReplicationUser.Factory ruf,
+      final SchemaFactory<ReviewDb> db) throws ConfigInvalidException,
+      IOException {
     injector = i;
     workQueue = wq;
+    database = db;
+    replicationUserFactory = ruf;
     configs = allConfigs(sitePath);
   }
 
@@ -152,7 +168,8 @@
         c.addPushRefSpec(spec);
       }
 
-      r.add(new ReplicationConfig(injector, workQueue, c, cfg));
+      r.add(new ReplicationConfig(injector, workQueue, c, cfg, database,
+          replicationUserFactory));
     }
     return Collections.unmodifiableList(r);
   }
@@ -285,9 +302,13 @@
     private final WorkQueue.Executor pool;
     private final Map<URIish, PushOp> pending = new HashMap<URIish, PushOp>();
     private final PushOp.Factory opFactory;
+    private final ProjectControl.Factory projectControlFactory;
+    private final boolean authEnabled;
 
     ReplicationConfig(final Injector injector, final WorkQueue workQueue,
-        final RemoteConfig rc, final Config cfg) {
+        final RemoteConfig rc, final Config cfg, SchemaFactory<ReviewDb> db,
+        final ReplicationUser.Factory replicationUserFactory) {
+
       remote = rc;
       delay = Math.max(0, getInt(rc, cfg, "replicationdelay", 15));
 
@@ -295,6 +316,22 @@
       final String poolName = "ReplicateTo-" + rc.getName();
       pool = workQueue.createQueue(poolSize, poolName);
 
+      String[] authGroupNames =
+          cfg.getStringList("remote", rc.getName(), "authGroup");
+      authEnabled = authGroupNames.length > 0;
+      Set<AccountGroup.Id> authGroups = groupsFor(db, authGroupNames);
+
+      final ReplicationUser remoteUser =
+          replicationUserFactory.create(authGroups);
+
+      projectControlFactory =
+          injector.createChildInjector(new AbstractModule() {
+            @Override
+            protected void configure() {
+              bind(CurrentUser.class).toInstance(remoteUser);
+            }
+          }).getInstance(ProjectControl.Factory.class);
+
       opFactory = injector.createChildInjector(new AbstractModule() {
         @Override
         protected void configure() {
@@ -307,6 +344,31 @@
       }).getInstance(PushOp.Factory.class);
     }
 
+    private static Set<AccountGroup.Id> groupsFor(
+        SchemaFactory<ReviewDb> dbfactory, String[] groupNames) {
+      final Set<AccountGroup.Id> result = new HashSet<AccountGroup.Id>();
+      try {
+        final ReviewDb db = dbfactory.open();
+        try {
+          for (String name : groupNames) {
+            AccountGroup group =
+                db.accountGroups().get(new AccountGroup.NameKey(name));
+            if (group == null) {
+              log.warn("Group \"" + name + "\" not in database,"
+                  + " removing from authGroup");
+            } else {
+              result.add(group.getId());
+            }
+          }
+        } finally {
+          db.close();
+        }
+      } catch (OrmException e) {
+        log.error("Database error: " + e);
+      }
+      return result;
+    }
+
     private int getInt(final RemoteConfig rc, final Config cfg,
         final String name, final int defValue) {
       return cfg.getInt("remote", rc.getName(), name, defValue);
@@ -314,6 +376,16 @@
 
     void schedule(final Project.NameKey project, final String ref,
         final URIish uri) {
+      try {
+        if (authEnabled
+            && !projectControlFactory.controlFor(project).isVisible()) {
+          return;
+        }
+      } catch (NoSuchProjectException e1) {
+        log.error("Internal error: project " + project
+            + " not found during replication");
+        return;
+      }
       synchronized (pending) {
         PushOp e = pending.get(uri);
         if (e == null) {
diff --git a/src/main/java/com/google/gerrit/server/AccessPath.java b/src/main/java/com/google/gerrit/server/AccessPath.java
index 08a577e..81888249 100644
--- a/src/main/java/com/google/gerrit/server/AccessPath.java
+++ b/src/main/java/com/google/gerrit/server/AccessPath.java
@@ -23,5 +23,8 @@
   WEB,
 
   /** Access through an SSH command, e.g. git fetch or push. */
-  SSH;
+  SSH,
+
+  /** Access through replication */
+  REPLICATION;
 }
diff --git a/src/main/java/com/google/gerrit/server/ReplicationUser.java b/src/main/java/com/google/gerrit/server/ReplicationUser.java
new file mode 100644
index 0000000..0885af8
--- /dev/null
+++ b/src/main/java/com/google/gerrit/server/ReplicationUser.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2009 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.server;
+
+import com.google.gerrit.client.reviewdb.AccountGroup;
+import com.google.gerrit.client.reviewdb.Change;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+public class ReplicationUser extends CurrentUser {
+  public interface Factory {
+    ReplicationUser create(@Assisted Set<AccountGroup.Id> authGroups);
+  }
+
+  private Set<AccountGroup.Id> effectiveGroups;
+
+  @Inject
+  protected ReplicationUser(AuthConfig authConfig,
+      @Assisted Set<AccountGroup.Id> authGroups) {
+    super(AccessPath.REPLICATION, authConfig);
+    effectiveGroups = new HashSet<AccountGroup.Id>(authGroups);
+
+    if (effectiveGroups.isEmpty()) {
+      effectiveGroups.addAll(authConfig.getRegisteredGroups());
+    }
+
+    effectiveGroups = Collections.unmodifiableSet(effectiveGroups);
+  }
+
+  @Override
+  public Set<AccountGroup.Id> getEffectiveGroups() {
+    return Collections.unmodifiableSet(effectiveGroups);
+  }
+
+  @Override
+  public Set<Change.Id> getStarredChanges() {
+    return Collections.emptySet();
+  }
+}
diff --git a/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 272a705..83a16ce 100644
--- a/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -35,6 +35,7 @@
 import com.google.gerrit.server.GerritPersonIdentProvider;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.MimeUtilFileTypeRegistry;
+import com.google.gerrit.server.ReplicationUser;
 import com.google.gerrit.server.account.AccountByEmailCacheImpl;
 import com.google.gerrit.server.account.AccountCacheImpl;
 import com.google.gerrit.server.account.AccountInfoCacheFactory;
@@ -45,7 +46,6 @@
 import com.google.gerrit.server.cache.CachePool;
 import com.google.gerrit.server.ldap.LdapModule;
 import com.google.gerrit.server.mail.AbandonedSender;
-import com.google.gerrit.server.mail.AddReviewerSender;
 import com.google.gerrit.server.mail.CommentSender;
 import com.google.gerrit.server.mail.EmailSender;
 import com.google.gerrit.server.mail.FromAddressGenerator;
@@ -160,5 +160,6 @@
     factory(MergedSender.Factory.class);
     factory(MergeFailSender.Factory.class);
     factory(RegisterNewEmailSender.Factory.class);
+    factory(ReplicationUser.Factory.class);
   }
 }