OnSubmitValidationListener: Don't expose Repository

If we give extensions a full Repository, it's possible to use it in an
incorrect way. For example, a RevWalk created with `new RevWalk(repo)`
will not be able to read the ObjectIds returned by `getCommands()`.
Instead, expose only a RevWalk and a getRef method that calls into the
ChainedReceiveCommands. This is more limiting, because it can't do
everything a Repository can do, but it is impossible to use in this
incorrect way.

The only known existing OnSubmitValidationListener is in the
git-numberer plugin[1], which will be able to do what it needs with this
restricted interface.

[1] https://chromium-review.googlesource.com/q/project:infra%252Fgerrit-plugins%252Fgit-numberer

Change-Id: I4a20ea34e49e30714e269b46835b55c43d7a121e
This commit is contained in:
Dave Borowitz
2017-03-22 18:29:21 -07:00
parent 4283674087
commit 563634e70a
5 changed files with 77 additions and 59 deletions

View File

@@ -17,6 +17,7 @@ package com.google.gerrit.acceptance.rest.change;
import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assert_; import static com.google.common.truth.Truth.assert_;
import static com.google.common.truth.Truth8.assertThat;
import static com.google.common.truth.TruthJUnit.assume; import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION; import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS; import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
@@ -95,6 +96,7 @@ import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@@ -756,11 +758,16 @@ public abstract class AbstractSubmit extends AbstractDaemonTest {
new OnSubmitValidationListener() { new OnSubmitValidationListener() {
@Override @Override
public void preBranchUpdate(Arguments args) throws ValidationException { public void preBranchUpdate(Arguments args) throws ValidationException {
assertThat(args.getCommands().keySet()).contains("refs/heads/master"); String master = "refs/heads/master";
try (RevWalk rw = args.newRevWalk()) { assertThat(args.getCommands()).containsKey(master);
rw.parseBody(rw.parseCommit(args.getCommands().get("refs/heads/master").getNewId())); ReceiveCommand cmd = args.getCommands().get(master);
ObjectId newMasterId = cmd.getNewId();
try (Repository repo = repoManager.openRepository(project)) {
assertThat(repo.exactRef(master).getObjectId()).isEqualTo(cmd.getOldId());
assertThat(args.getRef(master)).hasValue(newMasterId);
args.getRevWalk().parseBody(args.getRevWalk().parseCommit(newMasterId));
} catch (IOException e) { } catch (IOException e) {
assertThat(e).isNull(); throw new AssertionError("failed checking new ref value", e);
} }
projectsCalled.add(args.getProject().get()); projectsCalled.add(args.getProject().get());
if (projectsCalled.size() == 2) { if (projectsCalled.size() == 2) {

View File

@@ -11,15 +11,20 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package com.google.gerrit.server.git.validators; package com.google.gerrit.server.git.validators;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.extensions.annotations.ExtensionPoint; import com.google.gerrit.extensions.annotations.ExtensionPoint;
import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.Project.NameKey; import com.google.gerrit.server.git.RefCache;
import com.google.gerrit.server.update.ChainedReceiveCommands;
import com.google.gerrit.server.validators.ValidationException; import com.google.gerrit.server.validators.ValidationException;
import java.util.Map; import java.io.IOException;
import org.eclipse.jgit.lib.ObjectReader; import java.util.Optional;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand; import org.eclipse.jgit.transport.ReceiveCommand;
@@ -37,41 +42,58 @@ import org.eclipse.jgit.transport.ReceiveCommand;
public interface OnSubmitValidationListener { public interface OnSubmitValidationListener {
class Arguments { class Arguments {
private Project.NameKey project; private Project.NameKey project;
private Repository repository; private RevWalk rw;
private ObjectReader objectReader; private ImmutableMap<String, ReceiveCommand> commands;
private Map<String, ReceiveCommand> commands; private RefCache refs;
public Arguments( /**
NameKey project, * @param project project.
Repository repository, * @param rw revwalk that can read unflushed objects from {@code refs}.
ObjectReader objectReader, * @param commands commands to be executed.
Map<String, ReceiveCommand> commands) { */
this.project = project; Arguments(Project.NameKey project, RevWalk rw, ChainedReceiveCommands commands) {
this.repository = repository; this.project = checkNotNull(project);
this.objectReader = objectReader; this.rw = checkNotNull(rw);
this.commands = commands; this.refs = checkNotNull(commands);
this.commands = ImmutableMap.copyOf(commands.getCommands());
} }
/** Get the project name for this operation. */
public Project.NameKey getProject() { public Project.NameKey getProject() {
return project; return project;
} }
/** @return a read only repository */ /**
public Repository getRepository() { * Get a revwalk for this operation.
return repository; *
} * <p>This instance is able to read all objects mentioned in {@link #getCommands()} and {@link
* #getRef(String)}.
public RevWalk newRevWalk() { *
return new RevWalk(objectReader); * @return open revwalk.
*/
public RevWalk getRevWalk() {
return rw;
} }
/** /**
* @return a map from ref to op on it covering all ref ops to be performed on this repository as * @return a map from ref to commands covering all ref operations to be performed on this
* part of ongoing submit operation. * repository as part of the ongoing submit operation.
*/ */
public Map<String, ReceiveCommand> getCommands() { public ImmutableMap<String, ReceiveCommand> getCommands() {
return commands; return commands;
} }
/**
* Get a ref from the repository.
*
* @param name ref name; can be any ref, not just the ones mentioned in {@link #getCommands()}.
* @return latest value of a ref in the repository, as if all commands from {@link
* #getCommands()} had already been applied.
* @throws IOException if an error occurred reading the ref.
*/
public Optional<ObjectId> getRef(String name) throws IOException {
return refs.get(name);
}
} }
/** /**

View File

@@ -11,18 +11,18 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package com.google.gerrit.server.git.validators; package com.google.gerrit.server.git.validators;
import com.google.gerrit.extensions.registration.DynamicSet; import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.git.IntegrationException; import com.google.gerrit.server.git.IntegrationException;
import com.google.gerrit.server.git.validators.OnSubmitValidationListener.Arguments; import com.google.gerrit.server.git.validators.OnSubmitValidationListener.Arguments;
import com.google.gerrit.server.update.ChainedReceiveCommands;
import com.google.gerrit.server.validators.ValidationException; import com.google.gerrit.server.validators.ValidationException;
import com.google.inject.Inject; import com.google.inject.Inject;
import java.util.Map;
import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
public class OnSubmitValidators { public class OnSubmitValidators {
public interface Factory { public interface Factory {
@@ -37,14 +37,12 @@ public class OnSubmitValidators {
} }
public void validate( public void validate(
Project.NameKey project, Project.NameKey project, ObjectReader objectReader, ChainedReceiveCommands commands)
Repository repo,
ObjectReader objectReader,
Map<String, ReceiveCommand> commands)
throws IntegrationException { throws IntegrationException {
try { try (RevWalk rw = new RevWalk(objectReader)) {
for (OnSubmitValidationListener listener : this.listeners) { Arguments args = new Arguments(project, rw, commands);
listener.preBranchUpdate(new Arguments(project, repo, objectReader, commands)); for (OnSubmitValidationListener listener : listeners) {
listener.preBranchUpdate(args);
} }
} catch (ValidationException e) { } catch (ValidationException e) {
throw new IntegrationException(e.getMessage()); throw new IntegrationException(e.getMessage());

View File

@@ -313,16 +313,11 @@ class NoteDbBatchUpdate extends BatchUpdate {
} }
if (onSubmitValidators != null && commands != null && !commands.isEmpty()) { if (onSubmitValidators != null && commands != null && !commands.isEmpty()) {
// Validation of refs has to take place here and not at the beginning // Validation of refs has to take place here and not at the beginning of executeRefUpdates.
// executeRefUpdates. Otherwise failing validation in a second // Otherwise, failing validation in a second BatchUpdate object will happen *after* the
// BatchUpdate object will happen *after* first object's // first update's executeRefUpdates has finished, hence after first repo's refs have been
// executeRefUpdates has finished, hence after first repo's refs have // updated, which is too late.
// been updated, which is too late. onSubmitValidators.validate(project, ctx.getRevWalk().getObjectReader(), commands);
onSubmitValidators.validate(
project,
new ReadOnlyRepository(getRepository()),
ctx.getInserter().newReader(),
commands.getCommands());
} }
// TODO(dborowitz): Don't flush when fusing phases. // TODO(dborowitz): Don't flush when fusing phases.

View File

@@ -79,7 +79,6 @@ import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.RevWalk;
@@ -399,14 +398,11 @@ class ReviewDbBatchUpdate extends BatchUpdate {
} }
if (onSubmitValidators != null && commands != null && !commands.isEmpty()) { if (onSubmitValidators != null && commands != null && !commands.isEmpty()) {
try (ObjectReader reader = ctx.getInserter().newReader()) { // Validation of refs has to take place here and not at the beginning of executeRefUpdates.
// Validation of refs has to take place here and not at the beginning // Otherwise, failing validation in a second BatchUpdate object will happen *after* the
// executeRefUpdates. Otherwise failing validation in a second BatchUpdate object will // first update's executeRefUpdates has finished, hence after first repo's refs have been
// happen *after* first object's executeRefUpdates has finished, hence after first repo's // updated, which is too late.
// refs have been updated, which is too late. onSubmitValidators.validate(project, ctx.getRevWalk().getObjectReader(), commands);
onSubmitValidators.validate(
project, new ReadOnlyRepository(getRepository()), reader, commands.getCommands());
}
} }
if (inserter != null) { if (inserter != null) {