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);
}
}