diff --git a/Documentation/config-validation.txt b/Documentation/config-validation.txt index b7843a736d..5d23c798d7 100644 --- a/Documentation/config-validation.txt +++ b/Documentation/config-validation.txt @@ -32,6 +32,19 @@ are merged to the git repository. 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 42203fb06e..0c4f60cbfd 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.ReceivePackInitializer; 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 @@ public class GitOverHttpServlet extends GitServlet { private final Provider db; private final TagCache tagCache; private final ChangeCache changeCache; + private final UploadValidators.Factory uploadValidatorsFactory; @Inject - UploadFilter(Provider db, TagCache tagCache, ChangeCache changeCache) { + UploadFilter(Provider 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 @@ public class GitOverHttpServlet extends GitServlet { "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 729335bc16..dfd24468b1 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.CommitValidators; 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 @@ public class GerritGlobalModule extends FactoryModule { 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 0000000000..159496b1fe --- /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 0000000000..fefe02ad99 --- /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 wants, + Collection 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 0000000000..1735d2817f --- /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 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 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 wants, + Collection 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 wants, int cntOffered) + throws ServiceMayNotContinueException { + } + + @Override + public void onEndNegotiateRound(UploadPack up, + Collection 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 2e3d1132f3..f055b2ff4f 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 @@ public class SshSession { return identity; } + public SocketAddress getRemoteAddress() { + return remoteAddress; + } + + public String getRemoteAddressAsString() { + return remoteAsString; + } + String getUsername() { return username; } @@ -94,14 +102,6 @@ public class SshSession { 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 ff3826bad8..34f7107059 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.ChangeCache; 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.PreUploadHookChain; 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 @@ final class Upload extends AbstractGitCommand { @Inject private DynamicSet preUploadHooks; + @Inject + private UploadValidators.Factory uploadValidatorsFactory; + + @Inject + private SshSession session; + @Override protected void runImpl() throws IOException, Failure { if (!projectControl.canRunUploadPack()) { @@ -61,8 +71,21 @@ final class Upload extends AbstractGitCommand { } up.setPackConfig(config.getPackConfig()); up.setTimeout(config.getTimeout()); - up.setPreUploadHook(PreUploadHookChain.newChain( - Lists.newArrayList(preUploadHooks))); - up.upload(in, out, err); + + List 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()); + } + } } }