diff --git a/Documentation/config-validation.txt b/Documentation/config-validation.txt index 27b39ebbda..2707e5c898 100644 --- a/Documentation/config-validation.txt +++ b/Documentation/config-validation.txt @@ -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 diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java index 21fe05cbd6..2f73360acb 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java @@ -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 messages = new ArrayList<>(); + private final List messages = new ArrayList<>(); private ListMultimap 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() diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java index 674e7883c1..c7b29228c4 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java @@ -55,6 +55,7 @@ public class CreateBranch implements RestModifyView db; private final GitReferenceUpdated referenceUpdated; + private final RefValidationHelper refCreationValidator; private String ref; @Inject @@ -62,11 +63,14 @@ public class CreateBranch implements RestModifyView 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 { private final GitRepositoryManager repoManager; private final Provider queryProvider; private final GitReferenceUpdated referenceUpdated; + private final RefValidationHelper refDeletionValidator; @Inject DeleteBranch(Provider identifiedUser, GitRepositoryManager repoManager, Provider 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 { 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 { diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java index e0a84eb004..f4fa4468ee 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java @@ -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 queryProvider; private final GitReferenceUpdated referenceUpdated; + private final RefValidationHelper refDeletionValidator; @Inject DeleteBranches(Provider identifiedUser, GitRepositoryManager repoManager, Provider 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; } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java index 970629823e..8a6145a859 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java @@ -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); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefValidationHelper.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefValidationHelper.java new file mode 100644 index 0000000000..6e2fd5dcd8 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefValidationHelper.java @@ -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()); + } + } +}