Add config and rule to ignore self approval on labels

A frequently used Prolog rule is to ignore self approvals when
evaluating labels. The simple-submit-rules plugin has created such a
rule in Java instead of Prolog. All of the other label functions are
handled directly in core so it seems like a good fit, to also just port
this one into Gerrit core given that it is used quite often.

The new rule gets added as a separate rule so that we grow the
SubmitRules implementations more atomically (instead of just adding it
to DefaultRule).

We add tests that are mainly also just ported from simple-submit-rules.
Besides BUILD file and reference changes, the code was migrated to use
Flogger and we are more indicative to the user in case the uploader is the
only one who approved the change by adding an additional requirement.

The rule uses the uploader to make it easier to explain it to users.
Basing it on the author would come with the confusion that when you
cherry pick a change, you can self approve in case the author of the
original patch set was someone else. Basing it on the change owner would
mean you can hijack any change by uploading a patch set to submit
self-reviewed code.

Change-Id: I78b97f60ca1bb4aaf5386103c9228f76974feea4
This commit is contained in:
Patrick Hiesel 2018-09-03 13:19:08 +02:00
parent 460a3f3c35
commit 12c87c7b78
9 changed files with 402 additions and 0 deletions

View File

@ -374,6 +374,15 @@ Branch can be a ref pattern similar to what is documented
link:access-control.html#reference[here], but must not contain `${username}` or
`${shardeduserid}`.
[[label_ignoreSelfApproval]]
=== `label.Label-Name.ignoreSelfApproval`
If true, the label may be voted on by the uploader of the latest patch set,
but their approval does not make a change submittable. Instead, a
non-uploader who has the right to vote has to approve the change.
Defaults to false.
[[label_example]]
=== Example

View File

@ -36,6 +36,7 @@ public class LabelType {
public static final boolean DEF_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE = false;
public static final boolean DEF_COPY_MAX_SCORE = false;
public static final boolean DEF_COPY_MIN_SCORE = false;
public static final boolean DEF_IGNORE_SELF_APPROVAL = false;
public static LabelType withDefaultValues(String name) {
checkName(name);
@ -103,6 +104,7 @@ public class LabelType {
protected boolean copyAllScoresIfNoCodeChange;
protected boolean copyAllScoresIfNoChange;
protected boolean allowPostSubmit;
protected boolean ignoreSelfApproval;
protected short defaultValue;
protected List<LabelValue> values;
@ -141,6 +143,7 @@ public class LabelType {
setCopyMaxScore(DEF_COPY_MAX_SCORE);
setCopyMinScore(DEF_COPY_MIN_SCORE);
setAllowPostSubmit(DEF_ALLOW_POST_SUBMIT);
setIgnoreSelfApproval(DEF_IGNORE_SELF_APPROVAL);
byValue = new HashMap<>();
for (LabelValue v : values) {
@ -188,6 +191,14 @@ public class LabelType {
this.allowPostSubmit = allowPostSubmit;
}
public boolean ignoreSelfApproval() {
return ignoreSelfApproval;
}
public void setIgnoreSelfApproval(boolean ignoreSelfApproval) {
this.ignoreSelfApproval = ignoreSelfApproval;
}
public void setRefPatterns(List<String> refPatterns) {
if (refPatterns != null) {
this.refPatterns =

View File

@ -75,6 +75,7 @@ import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryProcessor;
import com.google.gerrit.server.restapi.group.GroupModule;
import com.google.gerrit.server.rules.DefaultSubmitRule;
import com.google.gerrit.server.rules.IgnoreSelfApprovalRule;
import com.google.gerrit.server.rules.PrologModule;
import com.google.gerrit.server.rules.SubmitRule;
import com.google.gerrit.server.update.BatchUpdate;
@ -188,6 +189,7 @@ public class BatchProgramModule extends FactoryModule {
factory(SubmitRuleEvaluator.Factory.class);
install(new PrologModule());
install(new DefaultSubmitRule.Module());
install(new IgnoreSelfApprovalRule.Module());
bind(ChangeJson.Factory.class).toProvider(Providers.<ChangeJson.Factory>of(null));
bind(EventUtil.class).toProvider(Providers.<EventUtil>of(null));

View File

@ -170,6 +170,7 @@ import com.google.gerrit.server.query.change.ConflictsCacheImpl;
import com.google.gerrit.server.restapi.change.SuggestReviewers;
import com.google.gerrit.server.restapi.group.GroupModule;
import com.google.gerrit.server.rules.DefaultSubmitRule;
import com.google.gerrit.server.rules.IgnoreSelfApprovalRule;
import com.google.gerrit.server.rules.PrologModule;
import com.google.gerrit.server.rules.RulesCache;
import com.google.gerrit.server.rules.SubmitRule;
@ -244,6 +245,7 @@ public class GerritGlobalModule extends FactoryModule {
install(new NoteDbModule(cfg));
install(new PrologModule());
install(new DefaultSubmitRule.Module());
install(new IgnoreSelfApprovalRule.Module());
install(new ReceiveCommitsModule());
install(new SshAddressesModule());
install(ThreadLocalRequestContext.module());

View File

@ -91,6 +91,7 @@ public class ProjectConfig extends VersionedMetaData implements ValidationError.
public static final String KEY_DEFAULT_VALUE = "defaultValue";
public static final String KEY_COPY_MIN_SCORE = "copyMinScore";
public static final String KEY_ALLOW_POST_SUBMIT = "allowPostSubmit";
public static final String KEY_IGNORE_SELF_APPROVAL = "ignoreSelfApproval";
public static final String KEY_COPY_MAX_SCORE = "copyMaxScore";
public static final String KEY_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE =
"copyAllScoresOnMergeFirstParentUpdate";
@ -882,6 +883,8 @@ public class ProjectConfig extends VersionedMetaData implements ValidationError.
}
label.setAllowPostSubmit(
rc.getBoolean(LABEL, name, KEY_ALLOW_POST_SUBMIT, LabelType.DEF_ALLOW_POST_SUBMIT));
label.setIgnoreSelfApproval(
rc.getBoolean(LABEL, name, KEY_IGNORE_SELF_APPROVAL, LabelType.DEF_IGNORE_SELF_APPROVAL));
label.setCopyMinScore(
rc.getBoolean(LABEL, name, KEY_COPY_MIN_SCORE, LabelType.DEF_COPY_MIN_SCORE));
label.setCopyMaxScore(
@ -1317,6 +1320,13 @@ public class ProjectConfig extends VersionedMetaData implements ValidationError.
KEY_ALLOW_POST_SUBMIT,
label.allowPostSubmit(),
LabelType.DEF_ALLOW_POST_SUBMIT);
setBooleanConfigKey(
rc,
LABEL,
name,
KEY_IGNORE_SELF_APPROVAL,
label.ignoreSelfApproval(),
LabelType.DEF_IGNORE_SELF_APPROVAL);
setBooleanConfigKey(
rc,
LABEL,

View File

@ -0,0 +1,171 @@
// Copyright (C) 2018 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.rules;
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.data.LabelFunction;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.common.data.SubmitRequirement;
import com.google.gerrit.extensions.annotations.Exports;
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Rule to require an approval from a user that did not upload the current patch set or block
* submission.
*/
@Singleton
public class IgnoreSelfApprovalRule implements SubmitRule {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final String E_UNABLE_TO_FETCH_UPLOADER = "Unable to fetch uploader";
private static final String E_UNABLE_TO_FETCH_LABELS =
"Unable to fetch labels and approvals for the change";
public static class Module extends FactoryModule {
@Override
public void configure() {
bind(SubmitRule.class)
.annotatedWith(Exports.named("IgnoreSelfApprovalRule"))
.to(IgnoreSelfApprovalRule.class);
}
}
@Inject
IgnoreSelfApprovalRule() {}
@Override
public Collection<SubmitRecord> evaluate(ChangeData cd, SubmitRuleOptions options) {
List<LabelType> labelTypes;
List<PatchSetApproval> approvals;
try {
labelTypes = cd.getLabelTypes().getLabelTypes();
approvals = cd.currentApprovals();
} catch (OrmException e) {
logger.atWarning().withCause(e).log(E_UNABLE_TO_FETCH_LABELS);
return singletonRuleError(E_UNABLE_TO_FETCH_LABELS);
}
boolean shouldIgnoreSelfApproval = labelTypes.stream().anyMatch(l -> l.ignoreSelfApproval());
if (!shouldIgnoreSelfApproval) {
// Shortcut to avoid further processing if no label should ignore uploader approvals
return ImmutableList.of();
}
Account.Id uploader;
try {
uploader = cd.currentPatchSet().getUploader();
} catch (OrmException e) {
logger.atWarning().withCause(e).log(E_UNABLE_TO_FETCH_UPLOADER);
return singletonRuleError(E_UNABLE_TO_FETCH_UPLOADER);
}
SubmitRecord submitRecord = new SubmitRecord();
submitRecord.status = SubmitRecord.Status.OK;
submitRecord.labels = new ArrayList<>(labelTypes.size());
submitRecord.requirements = new ArrayList<>();
for (LabelType t : labelTypes) {
if (!t.ignoreSelfApproval()) {
// The default rules are enough in this case.
continue;
}
LabelFunction labelFunction = t.getFunction();
if (labelFunction == null) {
continue;
}
Collection<PatchSetApproval> allApprovalsForLabel = filterApprovalsByLabel(approvals, t);
SubmitRecord.Label allApprovalsCheckResult = labelFunction.check(t, allApprovalsForLabel);
SubmitRecord.Label ignoreSelfApprovalCheckResult =
labelFunction.check(t, filterOutPositiveApprovalsOfUser(allApprovalsForLabel, uploader));
if (labelCheckPassed(allApprovalsCheckResult)
&& !labelCheckPassed(ignoreSelfApprovalCheckResult)) {
// The label has a valid approval from the uploader and no other valid approval. Set the
// label
// to NOT_READY and indicate the need for non-uploader approval as requirement.
submitRecord.labels.add(ignoreSelfApprovalCheckResult);
submitRecord.status = SubmitRecord.Status.NOT_READY;
// Add an additional requirement to be more descriptive on why the label counts as not
// approved.
submitRecord.requirements.add(
SubmitRequirement.builder()
.setFallbackText("Approval from non-uploader required")
.setType("non_uploader_approval")
.build());
}
}
if (submitRecord.labels.isEmpty()) {
return ImmutableList.of();
}
return ImmutableList.of(submitRecord);
}
private static boolean labelCheckPassed(SubmitRecord.Label label) {
switch (label.status) {
case OK:
case MAY:
return true;
case NEED:
case REJECT:
case IMPOSSIBLE:
return false;
}
return false;
}
private static Collection<SubmitRecord> singletonRuleError(String reason) {
SubmitRecord submitRecord = new SubmitRecord();
submitRecord.errorMessage = reason;
submitRecord.status = SubmitRecord.Status.RULE_ERROR;
return ImmutableList.of(submitRecord);
}
@VisibleForTesting
static Collection<PatchSetApproval> filterOutPositiveApprovalsOfUser(
Collection<PatchSetApproval> approvals, Account.Id user) {
return approvals
.stream()
.filter(input -> input.getValue() < 0 || !input.getAccountId().equals(user))
.collect(toImmutableList());
}
@VisibleForTesting
static Collection<PatchSetApproval> filterApprovalsByLabel(
Collection<PatchSetApproval> approvals, LabelType t) {
return approvals
.stream()
.filter(input -> input.getLabelId().get().equals(t.getLabelId().get()))
.collect(toImmutableList());
}
}

View File

@ -0,0 +1,99 @@
// Copyright (C) 2018 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.acceptance.server.rules;
import static com.google.common.truth.Truth.assertThat;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.common.data.SubmitRequirement;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.rules.IgnoreSelfApprovalRule;
import com.google.inject.Inject;
import java.util.Collection;
import java.util.Map;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.junit.Test;
@NoHttpd
public class IgnoreSelfApprovalRuleIT extends AbstractDaemonTest {
@Inject private IgnoreSelfApprovalRule rule;
@Test
public void blocksWhenUploaderIsOnlyApprover() throws Exception {
enableRule("Code-Review", true);
PushOneCommit.Result r = createChange();
approve(r.getChangeId());
Collection<SubmitRecord> submitRecords =
rule.evaluate(r.getChange(), SubmitRuleOptions.defaults());
assertThat(submitRecords).hasSize(1);
SubmitRecord result = submitRecords.iterator().next();
assertThat(result.status).isEqualTo(SubmitRecord.Status.NOT_READY);
assertThat(result.labels).isNotEmpty();
assertThat(result.requirements)
.containsExactly(
SubmitRequirement.builder()
.setFallbackText("Approval from non-uploader required")
.setType("non_uploader_approval")
.build());
}
@Test
public void allowsSubmissionWhenChangeHasNonUploaderApproval() throws Exception {
enableRule("Code-Review", true);
// Create change as user
TestRepository<InMemoryRepository> userTestRepo = cloneProject(project, user);
PushOneCommit push = pushFactory.create(db, user.getIdent(), userTestRepo);
PushOneCommit.Result r = push.to("refs/for/master");
// Approve as admin
approve(r.getChangeId());
Collection<SubmitRecord> submitRecords =
rule.evaluate(r.getChange(), SubmitRuleOptions.defaults());
assertThat(submitRecords).isEmpty();
}
@Test
public void doesNothingByDefault() throws Exception {
enableRule("Code-Review", false);
PushOneCommit.Result r = createChange();
approve(r.getChangeId());
Collection<SubmitRecord> submitRecords =
rule.evaluate(r.getChange(), SubmitRuleOptions.defaults());
assertThat(submitRecords).isEmpty();
}
private void enableRule(String labelName, boolean newState) throws Exception {
try (ProjectConfigUpdate u = updateProject(project)) {
Map<String, LabelType> localLabelSections = u.getConfig().getLabelSections();
if (localLabelSections.isEmpty()) {
localLabelSections.putAll(projectCache.getAllProjects().getConfig().getLabelSections());
}
localLabelSections.get(labelName).setIgnoreSelfApproval(newState);
u.save();
}
}
}

View File

@ -7,9 +7,11 @@ junit_tests(
resources = ["//prologtests:gerrit_common_test"],
deps = [
"//java/com/google/gerrit/common:server",
"//java/com/google/gerrit/reviewdb:server",
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/server/project/testing:project-test-util",
"//java/com/google/gerrit/testing:gerrit-test-util",
"//lib:guava",
"//lib/guice",
"//lib/jgit/org.eclipse.jgit:jgit",
"//lib/prolog:runtime",

View File

@ -0,0 +1,96 @@
// Copyright (C) 2018 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.rules;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelValue;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.LabelId;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import org.junit.Test;
public class IgnoreSelfApprovalRuleTest {
private static final Change.Id CHANGE_ID = new Change.Id(100);
private static final PatchSet.Id PS_ID = new PatchSet.Id(CHANGE_ID, 1);
private static final LabelType VERIFIED = makeLabel("Verified");
private static final Account.Id USER1 = makeAccount(100001);
@Test
public void filtersByLabel() {
LabelType codeReview = makeLabel("Code-Review");
PatchSetApproval approvalVerified = makeApproval(VERIFIED.getLabelId(), USER1, 2);
PatchSetApproval approvalCr = makeApproval(codeReview.getLabelId(), USER1, 2);
Collection<PatchSetApproval> filteredApprovals =
IgnoreSelfApprovalRule.filterApprovalsByLabel(
ImmutableList.of(approvalVerified, approvalCr), VERIFIED);
assertThat(filteredApprovals).containsExactly(approvalVerified);
}
@Test
public void filtersVotesFromUser() {
PatchSetApproval approvalM2 = makeApproval(VERIFIED.getLabelId(), USER1, -2);
PatchSetApproval approvalM1 = makeApproval(VERIFIED.getLabelId(), USER1, -1);
ImmutableList<PatchSetApproval> approvals =
ImmutableList.of(
approvalM2,
approvalM1,
makeApproval(VERIFIED.getLabelId(), USER1, 0),
makeApproval(VERIFIED.getLabelId(), USER1, +1),
makeApproval(VERIFIED.getLabelId(), USER1, +2));
Collection<PatchSetApproval> filteredApprovals =
IgnoreSelfApprovalRule.filterOutPositiveApprovalsOfUser(approvals, USER1);
assertThat(filteredApprovals).containsExactly(approvalM1, approvalM2);
}
private static LabelType makeLabel(String labelName) {
List<LabelValue> values = new ArrayList<>();
// The label text is irrelevant here, only the numerical value is used
values.add(new LabelValue((short) -2, "-2"));
values.add(new LabelValue((short) -1, "-1"));
values.add(new LabelValue((short) 0, "No vote."));
values.add(new LabelValue((short) 1, "+1"));
values.add(new LabelValue((short) 2, "+2"));
return new LabelType(labelName, values);
}
private static PatchSetApproval makeApproval(LabelId labelId, Account.Id accountId, int value) {
PatchSetApproval.Key key = makeKey(PS_ID, accountId, labelId);
return new PatchSetApproval(key, (short) value, Date.from(Instant.now()));
}
private static PatchSetApproval.Key makeKey(
PatchSet.Id psId, Account.Id accountId, LabelId labelId) {
return new PatchSetApproval.Key(psId, accountId, labelId);
}
private static Account.Id makeAccount(int account) {
return new Account.Id(account);
}
}