Add UploadValidationListener extension point.
Add pre-upload extension point to allow plugins to validate upload
operations right before Gerrit begins to send a pack back to the git
client.
A plugin can interrupt the upload by throwing an exception which will
cause the upload to fail and the exception’s message text will be
reported to the git client.
For example, a plugin may want to deny fetches for a specific project.
Change-Id: I5d0aeac51b83bf14150fa3036f71965cf2051243
diff --git a/Documentation/config-validation.txt b/Documentation/config-validation.txt
index b7843a7..5d23c79 100644
--- a/Documentation/config-validation.txt
+++ b/Documentation/config-validation.txt
@@ -32,6 +32,19 @@
If the commit fails the validation, the plugin can throw an exception
which will cause the merge to fail.
+[[pre-upload-validation]]
+== Pre-upload validation
+
+
+Plugins implementing the `UploadValidationListener` interface can
+perform additional validation checks before any upload operations
+(clone, fetch, pull). The validation is executed right before Gerrit
+begins to send a pack back to the git client.
+
+If upload fails the validation, the plugin can throw an exception
+which will cause the upload to fail and the exception's message text
+will be reported to the git client.
+
[[new-project-validation]]
== New project validation
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index 42203fb..0c4f60c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -33,6 +33,7 @@
import com.google.gerrit.server.git.TagCache;
import com.google.gerrit.server.git.TransferConfig;
import com.google.gerrit.server.git.VisibleRefFilter;
+import com.google.gerrit.server.git.validators.UploadValidators;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectControl;
import com.google.inject.AbstractModule;
@@ -213,12 +214,15 @@
private final Provider<ReviewDb> db;
private final TagCache tagCache;
private final ChangeCache changeCache;
+ private final UploadValidators.Factory uploadValidatorsFactory;
@Inject
- UploadFilter(Provider<ReviewDb> db, TagCache tagCache, ChangeCache changeCache) {
+ UploadFilter(Provider<ReviewDb> db, TagCache tagCache, ChangeCache changeCache,
+ UploadValidators.Factory uploadValidatorsFactory) {
this.db = db;
this.tagCache = tagCache;
this.changeCache = changeCache;
+ this.uploadValidatorsFactory = uploadValidatorsFactory;
}
@Override
@@ -235,9 +239,15 @@
"upload-pack not permitted on this server");
return;
}
-
+ // We use getRemoteHost() here instead of getRemoteAddr() because REMOTE_ADDR
+ // may have been overridden by a proxy server -- we'll try to avoid this.
+ UploadValidators uploadValidators =
+ uploadValidatorsFactory.create(pc.getProject(), repo, request.getRemoteHost());
+ up.setPreUploadHook(PreUploadHookChain.newChain(
+ Lists.newArrayList(up.getPreUploadHook(), uploadValidators)));
if (!pc.allRefsAreVisible()) {
- up.setAdvertiseRefsHook(new VisibleRefFilter(tagCache, changeCache, repo, pc, db.get(), true));
+ up.setAdvertiseRefsHook(new VisibleRefFilter(tagCache, changeCache,
+ repo, pc, db.get(), true));
}
next.doFilter(request, response);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 729335b..dfd2446 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -89,6 +89,8 @@
import com.google.gerrit.server.git.validators.MergeValidationListener;
import com.google.gerrit.server.git.validators.MergeValidators;
import com.google.gerrit.server.git.validators.MergeValidators.ProjectConfigValidator;
+import com.google.gerrit.server.git.validators.UploadValidationListener;
+import com.google.gerrit.server.git.validators.UploadValidators;
import com.google.gerrit.server.group.GroupModule;
import com.google.gerrit.server.mail.AddReviewerSender;
import com.google.gerrit.server.mail.CreateChangeSender;
@@ -276,6 +278,9 @@
DynamicSet.setOf(binder(), PatchSetWebLink.class);
DynamicSet.setOf(binder(), ProjectWebLink.class);
+ factory(UploadValidators.Factory.class);
+ DynamicSet.setOf(binder(), UploadValidationListener.class);
+
bind(AnonymousUser.class);
factory(CommitValidators.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidationException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidationException.java
new file mode 100644
index 0000000..159496b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidationException.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2014 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.git.validators;
+
+import org.eclipse.jgit.transport.ServiceMayNotContinueException;
+
+public class UploadValidationException extends ServiceMayNotContinueException {
+
+ private static final long serialVersionUID = 1L;
+
+ public UploadValidationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public UploadValidationException(String message) {
+ super(message);
+ }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidationListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidationListener.java
new file mode 100644
index 0000000..fefe02a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidationListener.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2014 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.git.validators;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.validators.ValidationException;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.UploadPack;
+
+import java.util.Collection;
+
+/**
+ * Listener to provide validation for upload operations.
+ *
+ * Invoked by Gerrit before it begins to send a pack to the client.
+ *
+ * Implementors can block the upload operation by throwing a
+ * ValidationException. The exception's message text will be reported to
+ * the end-user over the client's protocol connection.
+ */
+@ExtensionPoint
+public interface UploadValidationListener {
+
+ /**
+ * Validate an upload before it begins.
+ *
+ * @param repository The repository
+ * @param project The project
+ * @param remoteHost Remote address/hostname of the user
+ * @param wants The list of wanted objects. These may be RevObject or
+ * RevCommit if the processor parsed them. Implementors should not rely
+ * on the values being parsed.
+ * @param haves The list of common objects. Empty on an initial clone request.
+ * These may be RevObject or RevCommit if the processor parsed them.
+ * Implementors should not rely on the values being parsed.
+ * @throws ValidationException to block the upload and send a message
+ * back to the end-used over the client's protocol connection.
+ */
+ public void onPreUpload(Repository repository, Project project,
+ String remoteHost, UploadPack up, Collection<? extends ObjectId> wants,
+ Collection<? extends ObjectId> haves)
+ throws ValidationException;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidators.java
new file mode 100644
index 0000000..1735d28
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidators.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2014 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.git.validators;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.validators.ValidationException;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.PreUploadHook;
+import org.eclipse.jgit.transport.ServiceMayNotContinueException;
+import org.eclipse.jgit.transport.UploadPack;
+
+import java.util.Collection;
+
+import javax.inject.Inject;
+
+public class UploadValidators implements PreUploadHook {
+
+ private final DynamicSet<UploadValidationListener> uploadValidationListeners;
+ private final Project project;
+ private final Repository repository;
+ private final String remoteHost;
+
+ public interface Factory {
+ UploadValidators create(Project project, Repository repository,
+ String remoteAddress);
+ }
+
+ @Inject
+ UploadValidators(
+ DynamicSet<UploadValidationListener> uploadValidationListeners,
+ @Assisted Project project, @Assisted Repository repository,
+ @Assisted String remoteHost) {
+ this.uploadValidationListeners = uploadValidationListeners;
+ this.project = project;
+ this.repository = repository;
+ this.remoteHost = remoteHost;
+ }
+
+ @Override
+ public void onSendPack(UploadPack up, Collection<? extends ObjectId> wants,
+ Collection<? extends ObjectId> haves)
+ throws ServiceMayNotContinueException {
+ for (UploadValidationListener validator : uploadValidationListeners) {
+ try {
+ validator.onPreUpload(repository, project, remoteHost, up, wants, haves);
+ } catch (ValidationException e) {
+ throw new UploadValidationException(e.getMessage());
+ }
+
+ }
+ }
+
+ @Override
+ public void onBeginNegotiateRound(UploadPack up,
+ Collection<? extends ObjectId> wants, int cntOffered)
+ throws ServiceMayNotContinueException {
+ }
+
+ @Override
+ public void onEndNegotiateRound(UploadPack up,
+ Collection<? extends ObjectId> wants, int cntCommon, int cntNotFound,
+ boolean ready) throws ServiceMayNotContinueException {
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java
index 2e3d113..f055b2f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java
@@ -64,6 +64,14 @@
return identity;
}
+ public SocketAddress getRemoteAddress() {
+ return remoteAddress;
+ }
+
+ public String getRemoteAddressAsString() {
+ return remoteAsString;
+ }
+
String getUsername() {
return username;
}
@@ -94,14 +102,6 @@
return authError != null;
}
- SocketAddress getRemoteAddress() {
- return remoteAddress;
- }
-
- String getRemoteAddressAsString() {
- return remoteAsString;
- }
-
private static String format(final SocketAddress remote) {
if (remote instanceof InetSocketAddress) {
final InetSocketAddress sa = (InetSocketAddress) remote;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java
index ff3826b..34f7107 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java
@@ -21,7 +21,10 @@
import com.google.gerrit.server.git.TagCache;
import com.google.gerrit.server.git.TransferConfig;
import com.google.gerrit.server.git.VisibleRefFilter;
+import com.google.gerrit.server.git.validators.UploadValidationException;
+import com.google.gerrit.server.git.validators.UploadValidators;
import com.google.gerrit.sshd.AbstractGitCommand;
+import com.google.gerrit.sshd.SshSession;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -30,6 +33,7 @@
import org.eclipse.jgit.transport.UploadPack;
import java.io.IOException;
+import java.util.List;
/** Publishes Git repositories over SSH using the Git upload-pack protocol. */
final class Upload extends AbstractGitCommand {
@@ -48,6 +52,12 @@
@Inject
private DynamicSet<PreUploadHook> preUploadHooks;
+ @Inject
+ private UploadValidators.Factory uploadValidatorsFactory;
+
+ @Inject
+ private SshSession session;
+
@Override
protected void runImpl() throws IOException, Failure {
if (!projectControl.canRunUploadPack()) {
@@ -61,8 +71,21 @@
}
up.setPackConfig(config.getPackConfig());
up.setTimeout(config.getTimeout());
- up.setPreUploadHook(PreUploadHookChain.newChain(
- Lists.newArrayList(preUploadHooks)));
- up.upload(in, out, err);
+
+ List<PreUploadHook> allPreUploadHooks = Lists.newArrayList(preUploadHooks);
+ allPreUploadHooks.add(uploadValidatorsFactory.create(project, repo,
+ session.getRemoteAddressAsString()));
+ up.setPreUploadHook(PreUploadHookChain.newChain(allPreUploadHooks));
+ try {
+ up.upload(in, out, err);
+ } catch (UploadValidationException e) {
+ // UploadValidationException is used by the UploadValidators to
+ // stop the uploadPack. We do not want this exception to go beyond this
+ // point otherwise it would print a stacktrace in the logs and return an
+ // internal server error to the client.
+ if (!e.isOutput()) {
+ up.sendMessage(e.getMessage());
+ }
+ }
}
}