Enable plugin-based validation of user's ref related operations

After user is authorized to perform a given ref operation, but just
before it is performed, invoke validation from plugins.

Validation is invoked for ref create, delete and update (including
non-ff) operations caused by user through either push or call to
existing ref related Gerrit endpoints (create/delete branch/es).

Change-Id: I11cbe51f54e7d133b336ab503577653b6fe0f443
Signed-off-by: Jacek Centkowski <geminica.programs@gmail.com>
This commit is contained in:
Jacek Centkowski 2014-09-10 07:53:51 -04:00
parent bc425ff53b
commit 0b9424690d
7 changed files with 125 additions and 11 deletions

View File

@ -21,16 +21,18 @@ and cherry-pick buttons.
Out of the box, Gerrit includes a plugin that checks the length of the
subject and body lines of commit messages on uploaded commits.
[[ref-operation-validation]]
== Ref operation validation
[[user-ref-operations-validation]]
== User ref operations validation
Plugins implementing the `RefOperationValidationListener` interface can
perform additional validation checks against ref creation/deletion operation
before it is applied to the git repository.
perform additional validation checks against user ref operations (resulting
from either push or corresponding Gerrit REST/SSH endpoints call e.g.
create branch etc.). Namely including ref creation, deletion and update
(also non-fast-forward) before they are applied to the git repository.
If the ref operation fails the validation, the plugin can throw an exception
which will cause the operation to fail.
The plugin can throw an exception which will cause the operation to fail,
and prevent the ref update from being applied.
[[pre-merge-validation]]
== Pre-merge validation

View File

@ -100,6 +100,9 @@ import com.google.gerrit.server.git.MultiProgressMonitor.Task;
import com.google.gerrit.server.git.validators.CommitValidationException;
import com.google.gerrit.server.git.validators.CommitValidationMessage;
import com.google.gerrit.server.git.validators.CommitValidators;
import com.google.gerrit.server.git.validators.RefOperationValidationException;
import com.google.gerrit.server.git.validators.RefOperationValidators;
import com.google.gerrit.server.git.validators.ValidationMessage;
import com.google.gerrit.server.mail.MailUtil.MailRecipients;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.NotesMigration;
@ -289,6 +292,7 @@ public class ReceiveCommits {
private final ProjectCache projectCache;
private final String canonicalWebUrl;
private final CommitValidators.Factory commitValidatorsFactory;
private final RefOperationValidators.Factory refValidatorsFactory;
private final TagCache tagCache;
private final AccountCache accountCache;
private final ChangeInserter.Factory changeInserterFactory;
@ -329,7 +333,7 @@ public class ReceiveCommits {
private final NotesMigration notesMigration;
private final ChangeEditUtil editUtil;
private final List<CommitValidationMessage> messages = new ArrayList<>();
private final List<ValidationMessage> messages = new ArrayList<>();
private ListMultimap<Error, String> errors = LinkedListMultimap.create();
private Task newProgress;
private Task replaceProgress;
@ -355,6 +359,7 @@ public class ReceiveCommits {
@Nullable SearchingChangeCacheImpl changeCache,
ChangeInserter.Factory changeInserterFactory,
CommitValidators.Factory commitValidatorsFactory,
RefOperationValidators.Factory refValidatorsFactory,
@CanonicalWebUrl String canonicalWebUrl,
RequestScopePropagator requestScopePropagator,
SshInfo sshInfo,
@ -392,6 +397,7 @@ public class ReceiveCommits {
this.accountCache = accountCache;
this.changeInserterFactory = changeInserterFactory;
this.commitValidatorsFactory = commitValidatorsFactory;
this.refValidatorsFactory = refValidatorsFactory;
this.requestScopePropagator = requestScopePropagator;
this.sshInfo = sshInfo;
this.allProjectsName = allProjectsName;
@ -544,7 +550,7 @@ public class ReceiveCommits {
}
void sendMessages() {
for (CommitValidationMessage m : messages) {
for (ValidationMessage m : messages) {
if (m.isError()) {
messageSender.sendError(m.getMessage());
} else {
@ -1096,6 +1102,9 @@ public class ReceiveCommits {
RefControl ctl = projectControl.controlForRef(cmd.getRefName());
if (ctl.canCreate(db, rp.getRepository(), obj)) {
if (!validRefOperation(cmd)) {
return;
}
validateNewCommits(ctl, cmd);
batch.addCommand(cmd);
} else {
@ -1111,6 +1120,9 @@ public class ReceiveCommits {
return;
}
if (!validRefOperation(cmd)) {
return;
}
validateNewCommits(ctl, cmd);
batch.addCommand(cmd);
} else {
@ -1148,6 +1160,9 @@ public class ReceiveCommits {
errors.put(Error.DELETE_CHANGES, ctl.getRefName());
reject(cmd, "cannot delete changes");
} else if (ctl.canDelete()) {
if (!validRefOperation(cmd)) {
return;
}
batch.addCommand(cmd);
} else {
if (RefNames.REFS_CONFIG.equals(ctl.getRefName())) {
@ -1182,6 +1197,9 @@ public class ReceiveCommits {
}
if (ctl.canForceUpdate()) {
if (!validRefOperation(cmd)) {
return;
}
batch.setAllowNonFastForwards(true).addCommand(cmd);
} else {
cmd.setResult(REJECTED_NONFASTFORWARD, " need '"
@ -2399,6 +2417,21 @@ public class ReceiveCommits {
}
}
private boolean validRefOperation(ReceiveCommand cmd) {
RefOperationValidators refValidators =
refValidatorsFactory.create(getProject(), user, cmd);
try {
messages.addAll(refValidators.validateForRefOperation());
} catch (RefOperationValidationException e) {
messages.addAll(Lists.newArrayList(e.getMessages()));
reject(cmd, e.getMessage());
return false;
}
return true;
}
private void validateNewCommits(RefControl ctl, ReceiveCommand cmd) {
if (ctl.canForgeAuthor()
&& ctl.canForgeCommitter()

View File

@ -55,6 +55,7 @@ public class CreateBranch implements RestModifyView<ProjectResource, BranchInput
private final GitRepositoryManager repoManager;
private final Provider<ReviewDb> db;
private final GitReferenceUpdated referenceUpdated;
private final RefValidationHelper refCreationValidator;
private String ref;
@Inject
@ -62,11 +63,14 @@ public class CreateBranch implements RestModifyView<ProjectResource, BranchInput
GitRepositoryManager repoManager,
Provider<ReviewDb> db,
GitReferenceUpdated referenceUpdated,
RefValidationHelper.Factory refHelperFactory,
@Assisted String ref) {
this.identifiedUser = identifiedUser;
this.repoManager = repoManager;
this.db = db;
this.referenceUpdated = referenceUpdated;
this.refCreationValidator =
refHelperFactory.create(ReceiveCommand.Type.CREATE);
this.ref = ref;
}
@ -123,6 +127,8 @@ public class CreateBranch implements RestModifyView<ProjectResource, BranchInput
u.setNewObjectId(object.copy());
u.setRefLogIdent(identifiedUser.get().newRefLogIdent());
u.setRefLogMessage("created via REST from " + input.revision, false);
refCreationValidator.validateRefOperation(
rsrc.getName(), identifiedUser.get(), u);
final RefUpdate.Result result = u.update(rw);
switch (result) {
case FAST_FORWARD:

View File

@ -50,16 +50,20 @@ public class DeleteBranch implements RestModifyView<BranchResource, Input> {
private final GitRepositoryManager repoManager;
private final Provider<InternalChangeQuery> queryProvider;
private final GitReferenceUpdated referenceUpdated;
private final RefValidationHelper refDeletionValidator;
@Inject
DeleteBranch(Provider<IdentifiedUser> identifiedUser,
GitRepositoryManager repoManager,
Provider<InternalChangeQuery> queryProvider,
GitReferenceUpdated referenceUpdated) {
GitReferenceUpdated referenceUpdated,
RefValidationHelper.Factory refHelperFactory) {
this.identifiedUser = identifiedUser;
this.repoManager = repoManager;
this.queryProvider = queryProvider;
this.referenceUpdated = referenceUpdated;
this.refDeletionValidator =
refHelperFactory.create(ReceiveCommand.Type.DELETE);
}
@Override
@ -78,6 +82,8 @@ public class DeleteBranch implements RestModifyView<BranchResource, Input> {
RefUpdate.Result result;
RefUpdate u = r.updateRef(rsrc.getRef());
u.setForceUpdate(true);
refDeletionValidator.validateRefOperation(
rsrc.getName(), identifiedUser.get(), u);
int remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS;
for (;;) {
try {

View File

@ -35,6 +35,7 @@ import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
@ -53,16 +54,20 @@ public class DeleteBranches
private final GitRepositoryManager repoManager;
private final Provider<InternalChangeQuery> queryProvider;
private final GitReferenceUpdated referenceUpdated;
private final RefValidationHelper refDeletionValidator;
@Inject
DeleteBranches(Provider<IdentifiedUser> identifiedUser,
GitRepositoryManager repoManager,
Provider<InternalChangeQuery> queryProvider,
GitReferenceUpdated referenceUpdated) {
GitReferenceUpdated referenceUpdated,
RefValidationHelper.Factory refHelperFactory) {
this.identifiedUser = identifiedUser;
this.repoManager = repoManager;
this.queryProvider = queryProvider;
this.referenceUpdated = referenceUpdated;
this.refDeletionValidator =
refHelperFactory.create(ReceiveCommand.Type.DELETE);
}
@Override
@ -100,7 +105,8 @@ public class DeleteBranches
}
private ReceiveCommand createDeleteCommand(ProjectResource project,
Repository r, String branch) throws OrmException, IOException {
Repository r, String branch)
throws OrmException, IOException, ResourceConflictException {
Ref ref = r.getRefDatabase().getRef(branch);
ReceiveCommand command;
if (ref == null) {
@ -120,6 +126,10 @@ public class DeleteBranches
if (!queryProvider.get().setLimit(1).byBranchOpen(branchKey).isEmpty()) {
command.setResult(Result.REJECTED_OTHER_REASON, "it has open changes");
}
RefUpdate u = r.updateRef(branch);
u.setForceUpdate(true);
refDeletionValidator.validateRefOperation(
project.getName(), identifiedUser.get(), u);
return command;
}

View File

@ -69,6 +69,7 @@ public class Module extends RestApiModule {
post(PROJECT_KIND, "branches:delete").to(DeleteBranches.class);
factory(CreateBranch.Factory.class);
get(BRANCH_KIND, "mergeable").to(CheckMergeability.class);
factory(RefValidationHelper.Factory.class);
get(BRANCH_KIND, "reflog").to(GetReflog.class);
child(BRANCH_KIND, "files").to(FilesCollection.class);
get(FILE_KIND, "content").to(GetContent.class);

View File

@ -0,0 +1,56 @@
// 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.project;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.git.validators.RefOperationValidators;
import com.google.gerrit.server.validators.ValidationException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.transport.ReceiveCommand.Type;
public class RefValidationHelper {
public interface Factory {
RefValidationHelper create(Type operationType);
}
private final RefOperationValidators.Factory refValidatorsFactory;
private final Type operationType;
@Inject
RefValidationHelper(RefOperationValidators.Factory refValidatorsFactory,
@Assisted Type operationType) {
this.refValidatorsFactory = refValidatorsFactory;
this.operationType = operationType;
}
public void validateRefOperation(String projectName, IdentifiedUser user,
RefUpdate update) throws ResourceConflictException {
RefOperationValidators refValidators =
refValidatorsFactory.create(
new Project(new Project.NameKey(projectName)),
user,
RefOperationValidators.getCommand(update, operationType));
try {
refValidators.validateForRefOperation();
} catch (ValidationException e) {
throw new ResourceConflictException(e.getMessage());
}
}
}