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
This commit is contained in:
Hugo Arès 2014-04-17 08:35:16 -04:00
parent 572d542b3d
commit 0c14990f6d
8 changed files with 233 additions and 14 deletions

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {
}
}

View File

@ -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;

View File

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