Add a new 'authoremail' submit requirement predicate
The new predicate returns true if the change author's email address
matches a specific regular expression pattern. The new predicate is
exactly the same as the commit_author prolog predicate (see [1]) which
accepts regular expressions.
Note: we could've used the already available 'author' search operator
but that operator matches with both name/email addresses, allowing an
adversary to set their name in a way that might match a submit
requirement rule. For this reason we implemented this as a new submit
requirement predicate that only matches with email addresses.
[1] https://gerrit-review.googlesource.com/Documentation/prolog-change-facts.html
Release-Notes: Add new authoremail:<email_pattern> submit requirement operator
Change-Id: I28a5e8f7d5494f051aaa26b2c2b7c5eaceae19fb
diff --git a/Documentation/config-submit-requirements.txt b/Documentation/config-submit-requirements.txt
index 465cb53..1782159 100644
--- a/Documentation/config-submit-requirements.txt
+++ b/Documentation/config-submit-requirements.txt
@@ -143,6 +143,14 @@
[[submit_requirements_operators]]
=== Submit Requirements Operators
+[[operator_authoremail]]
+authoremail:'EMAIL_PATTERN'::
++
+An operator that returns true if the change author's email address matches a
+specific regular expression pattern. The
+link:http://www.brics.dk/automaton/[dk.brics.automaton library,role=external,window=_blank]
+is used for the evaluation of such patterns.
+
[[operator_is_true]]
is:true::
+
diff --git a/java/com/google/gerrit/server/query/change/RegexAuthorEmailPredicate.java b/java/com/google/gerrit/server/query/change/RegexAuthorEmailPredicate.java
new file mode 100644
index 0000000..22891bc
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/RegexAuthorEmailPredicate.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2022 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.query.change;
+
+import com.google.gerrit.index.query.QueryParseException;
+import dk.brics.automaton.RegExp;
+import dk.brics.automaton.RunAutomaton;
+
+/**
+ * A submit requirement predicate that matches with changes having the author email's address
+ * matching a specific regular expression pattern.
+ */
+public class RegexAuthorEmailPredicate extends SubmitRequirementPredicate {
+ protected final RunAutomaton authorEmailPattern;
+
+ public RegexAuthorEmailPredicate(String pattern) throws QueryParseException {
+ super("authoremail", pattern);
+
+ if (pattern.startsWith("^")) {
+ pattern = pattern.substring(1);
+ }
+
+ if (pattern.endsWith("$") && !pattern.endsWith("\\$")) {
+ pattern = pattern.substring(0, pattern.length() - 1);
+ }
+
+ try {
+ this.authorEmailPattern = new RunAutomaton(new RegExp(pattern).toAutomaton());
+ } catch (IllegalArgumentException e) {
+ throw new QueryParseException(String.format("invalid regular expression: %s", pattern), e);
+ }
+ }
+
+ @Override
+ public boolean match(ChangeData cd) {
+ return authorEmailPattern.run(cd.getAuthor().getEmailAddress());
+ }
+
+ @Override
+ public int getCost() {
+ return 1;
+ }
+}
diff --git a/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
index 1750be0..54e0dd6 100644
--- a/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
@@ -54,4 +54,9 @@
}
return super.is(value);
}
+
+ @Operator
+ public Predicate<ChangeData> authoremail(String who) throws QueryParseException {
+ return new RegexAuthorEmailPredicate(who);
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
index 0f27d93..a16efb9 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
@@ -26,7 +26,9 @@
import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelFunction;
@@ -37,6 +39,7 @@
import com.google.gerrit.entities.SubmitRequirementResult;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
@@ -60,6 +63,7 @@
@Inject private ProjectOperations projectOperations;
@Inject private Provider<InternalChangeQuery> changeQueryProvider;
@Inject private ExtensionRegistry extensionRegistry;
+ @Inject private RequestScopeOperations requestScopeOperations;
private ChangeData changeData;
private String changeId;
@@ -447,6 +451,52 @@
assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.SATISFIED);
}
+ @Test
+ public void byAuthorEmail() throws Exception {
+ TestAccount user2 =
+ accountCreator.create("Foo", "user@example.com", "User", /* displayName = */ null);
+ requestScopeOperations.setApiUser(user2.id());
+ ChangeInfo info =
+ gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get();
+ ChangeData cd =
+ changeQueryProvider
+ .get()
+ .byLegacyChangeId(Change.Id.tryParse(Integer.toString(info._number)).get())
+ .get(0);
+
+ // Match by email works
+ checkSubmitRequirementResult(
+ cd,
+ /* submittabilityExpr= */ "authoremail:\"^.*@example\\.com\"",
+ SubmitRequirementResult.Status.SATISFIED);
+ checkSubmitRequirementResult(
+ cd,
+ /* submittabilityExpr= */ "authoremail:\"^user@.*\\.com\"",
+ SubmitRequirementResult.Status.SATISFIED);
+
+ // Match by name does not work
+ checkSubmitRequirementResult(
+ cd,
+ /* submittabilityExpr= */ "authoremail:\"^Foo$\"",
+ SubmitRequirementResult.Status.UNSATISFIED);
+ checkSubmitRequirementResult(
+ cd,
+ /* submittabilityExpr= */ "authoremail:\"^User$\"",
+ SubmitRequirementResult.Status.UNSATISFIED);
+ }
+
+ private void checkSubmitRequirementResult(
+ ChangeData cd, String submittabilityExpr, SubmitRequirementResult.Status expectedStatus) {
+ SubmitRequirement sr =
+ createSubmitRequirement(
+ /* applicabilityExpr= */ "project:" + project.get(),
+ submittabilityExpr,
+ /* overrideExpr= */ "");
+
+ SubmitRequirementResult result = evaluator.evaluateRequirement(sr, cd);
+ assertThat(result.status()).isEqualTo(expectedStatus);
+ }
+
private void voteLabel(String changeId, String labelName, int score) throws RestApiException {
gApi.changes().id(changeId).current().review(new ReviewInput().label(labelName, score));
}