email validator: add options to reject upload based on email patterns

Similarly to allowing upload only when the committer and author email
matches given patterns, introduce rejecting upload based on patterns.

This effectively adds the options `rejectedAuthorEmailPattern` and
`rejectedCommitterEmailPattern` and basically copies the implementation
inverting the logic.

There could be made a bigger effort to deduplicate the code, yet there
is very little churn on the code base itself and a high risk to break
existing users by refactoring the code in the absence of more detailed
tests.

The documentation has been updated accordingly and slightly modified for
clarity and brevity.

Bug: 259663353
Change-Id: Ifaee5073df1e67cb9fd04da10e391b596b80998e
Signed-off-by: Matthias Maennich <maennich@google.com>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ChangeEmailValidator.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ChangeEmailValidator.java
index 005da44..328bfa9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ChangeEmailValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ChangeEmailValidator.java
@@ -52,6 +52,17 @@
                     "Commits with author email not matching one of these pattterns will be"
                         + " rejected."));
         bind(ProjectConfigEntry.class)
+            .annotatedWith(Exports.named(KEY_REJECTED_AUTHOR_EMAIL_PATTERN))
+            .toInstance(
+                new ProjectConfigEntry(
+                    "Author Email Pattern",
+                    null,
+                    ProjectConfigEntryType.ARRAY,
+                    null,
+                    false,
+                    "Commits with author email matching one of these pattterns will be"
+                        + " rejected."));
+        bind(ProjectConfigEntry.class)
             .annotatedWith(Exports.named(KEY_ALLOWED_COMMITTER_EMAIL_PATTERN))
             .toInstance(
                 new ProjectConfigEntry(
@@ -62,12 +73,25 @@
                     false,
                     "Commits with committer email not matching one of these patterns will be"
                         + " rejected."));
+        bind(ProjectConfigEntry.class)
+            .annotatedWith(Exports.named(KEY_REJECTED_COMMITTER_EMAIL_PATTERN))
+            .toInstance(
+                new ProjectConfigEntry(
+                    "Committer Email Pattern",
+                    null,
+                    ProjectConfigEntryType.ARRAY,
+                    null,
+                    false,
+                    "Commits with committer email matching one of these patterns will be"
+                        + " rejected."));
       }
     };
   }
 
   public static final String KEY_ALLOWED_AUTHOR_EMAIL_PATTERN = "allowedAuthorEmailPattern";
+  public static final String KEY_REJECTED_AUTHOR_EMAIL_PATTERN = "rejectedAuthorEmailPattern";
   public static final String KEY_ALLOWED_COMMITTER_EMAIL_PATTERN = "allowedCommitterEmailPattern";
+  public static final String KEY_REJECTED_COMMITTER_EMAIL_PATTERN = "rejectedCommitterEmailPattern";
   private final String pluginName;
   private final PluginConfigFactory cfgFactory;
   private final ValidatorConfig validatorConfig;
@@ -88,20 +112,40 @@
   }
 
   @VisibleForTesting
+  static String[] getRejectedAuthorEmailPatterns(PluginConfig cfg) {
+    return cfg.getStringList(KEY_REJECTED_AUTHOR_EMAIL_PATTERN);
+  }
+
+  @VisibleForTesting
   static String[] getAllowedCommitterEmailPatterns(PluginConfig cfg) {
     return cfg.getStringList(KEY_ALLOWED_COMMITTER_EMAIL_PATTERN);
   }
 
   @VisibleForTesting
-  static boolean isAuthorActive(PluginConfig cfg) {
+  static String[] getRejectedCommitterEmailPatterns(PluginConfig cfg) {
+    return cfg.getStringList(KEY_REJECTED_COMMITTER_EMAIL_PATTERN);
+  }
+
+  @VisibleForTesting
+  static boolean isAuthorAllowListActive(PluginConfig cfg) {
     return cfg.getStringList(KEY_ALLOWED_AUTHOR_EMAIL_PATTERN).length > 0;
   }
 
   @VisibleForTesting
-  static boolean isCommitterActive(PluginConfig cfg) {
+  static boolean isAuthorRejectListActive(PluginConfig cfg) {
+    return cfg.getStringList(KEY_REJECTED_AUTHOR_EMAIL_PATTERN).length > 0;
+  }
+
+  @VisibleForTesting
+  static boolean isCommitterAllowListActive(PluginConfig cfg) {
     return cfg.getStringList(KEY_ALLOWED_COMMITTER_EMAIL_PATTERN).length > 0;
   }
 
+  @VisibleForTesting
+  static boolean isCommitterRejectListActive(PluginConfig cfg) {
+    return cfg.getStringList(KEY_REJECTED_COMMITTER_EMAIL_PATTERN).length > 0;
+  }
+
   @Override
   public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
       throws CommitValidationException {
@@ -109,7 +153,7 @@
       PluginConfig cfg =
           cfgFactory.getFromProjectConfigWithInheritance(
               receiveEvent.project.getNameKey(), pluginName);
-      if (isAuthorActive(cfg)
+      if (isAuthorAllowListActive(cfg)
           && validatorConfig.isEnabled(
               receiveEvent.user,
               receiveEvent.getProjectNameKey(),
@@ -125,7 +169,23 @@
                   + "> - is not allowed for this Project.");
         }
       }
-      if (isCommitterActive(cfg)
+      if (isAuthorRejectListActive(cfg)
+          && validatorConfig.isEnabled(
+              receiveEvent.user,
+              receiveEvent.getProjectNameKey(),
+              receiveEvent.getRefName(),
+              KEY_REJECTED_AUTHOR_EMAIL_PATTERN,
+              receiveEvent.pushOptions)) {
+        if (match(
+            receiveEvent.commit.getAuthorIdent().getEmailAddress(),
+            getRejectedAuthorEmailPatterns(cfg))) {
+          throw new CommitValidationException(
+              "Author Email <"
+                  + receiveEvent.commit.getAuthorIdent().getEmailAddress()
+                  + "> - is not allowed for this Project.");
+        }
+      }
+      if (isCommitterAllowListActive(cfg)
           && validatorConfig.isEnabled(
               receiveEvent.user,
               receiveEvent.getProjectNameKey(),
@@ -141,6 +201,22 @@
                   + "> - is not allowed for this Project.");
         }
       }
+      if (isCommitterRejectListActive(cfg)
+          && validatorConfig.isEnabled(
+              receiveEvent.user,
+              receiveEvent.getProjectNameKey(),
+              receiveEvent.getRefName(),
+              KEY_REJECTED_COMMITTER_EMAIL_PATTERN,
+              receiveEvent.pushOptions)) {
+        if (match(
+            receiveEvent.commit.getCommitterIdent().getEmailAddress(),
+            getRejectedCommitterEmailPatterns(cfg))) {
+          throw new CommitValidationException(
+              "Committer Email <"
+                  + receiveEvent.commit.getCommitterIdent().getEmailAddress()
+                  + "> - is not allowed for this Project.");
+        }
+      }
     } catch (NoSuchProjectException e) {
       throw new CommitValidationException("Failed to check for Change Email Patterns ", e);
     }
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index aa7b0cf..9c5f967 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -34,35 +34,59 @@
     rejectDuplicatePathnamesLocale = en
     allowedAuthorEmailPattern = .*@example\\.com$
     allowedAuthorEmailPattern = admin@example\\.com
+    rejectedAuthorEmailPattern = .*@old-name\\.com
     allowedCommitterEmailPattern = .*gerrit\\.com
     allowedCommitterEmailPattern =  admin@gerrit\\..*
+    rejectedCommitterEmailPattern = .*@old-name\\.com
 ```
 
 plugin.@PLUGIN@.allowedAuthorEmailPattern
-:    Author Email to Allow.
+:    Author Email to allow.
 
     The check looks for a match based on the described specifics.
     If there are no matches the push will be rejected.
 
-    Note that all email addresses contain the dot character, and if
-    included in the pattern needs to be properly escaped as shown in
-    the examples.
+    Note that since most email addresses contain the dot character, it needs to
+    be properly escaped as part of a pattern to match. See the examples for
+    reference.
 
-    This check is using `java.util.regex.Pattern` which is described
-    [here][1].
+    This check is using [`java.util.regex.Pattern`][1].
+
+plugin.@PLUGIN@.rejectedAuthorEmailPattern
+:    Author Email to reject.
+
+    The check looks for a match based on the described specifics.
+    If there are any matches the push will be rejected.
+
+    Note that since most email addresses contain the dot character, it needs to
+    be properly escaped as part of a pattern to match. See the examples for
+    reference.
+
+    This check is using [`java.util.regex.Pattern`][1].
 
 plugin.@PLUGIN@.allowedCommitterEmailPattern
-:    Committer Email to Allow.
+:    Committer Email to allow.
 
     The check looks for a match based on the described specifics.
     If there are no matches the push will be rejected.
 
-    Note that all email addresses contain the dot character, and if
-    included in the pattern needs to be properly escaped as shown in
-    the examples.
+    Note that since most email addresses contain the dot character, it needs to
+    be properly escaped as part of a pattern to match. See the examples for
+    reference.
 
-    This check is using `java.util.regex.Pattern` which is described
-    [here][1].
+    This check is using [`java.util.regex.Pattern`][1].
+
+plugin.@PLUGIN@.rejectedCommitterEmailPattern
+:    Committer Email to reject.
+
+    The check looks for a match based on the described specifics.
+    If there are any matches the push will be rejected.
+
+    Note that since most email addresses contain the dot character, it needs to
+    be properly escaped as part of a pattern to match. See the examples for
+    reference.
+
+    This check is using [`java.util.regex.Pattern`][1].
 
 plugin.@PLUGIN@.blockedFileExtension
 :    File extension to be blocked.
diff --git a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/ChangeEmailTest.java b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/ChangeEmailTest.java
index d54658c..2c83f06 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/ChangeEmailTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/ChangeEmailTest.java
@@ -55,15 +55,28 @@
   }
 
   @Test
-  public void validatorBehaviorWhenAuthorConfigEmpty() {
-    assertThat(ChangeEmailValidator.isAuthorActive(EMPTY_PLUGIN_CONFIG)).isFalse();
+  public void validatorBehaviorWhenAuthorAllowListConfigEmpty() {
+    assertThat(ChangeEmailValidator.isAuthorAllowListActive(EMPTY_PLUGIN_CONFIG)).isFalse();
     assertThat(ChangeEmailValidator.getAllowedAuthorEmailPatterns(EMPTY_PLUGIN_CONFIG)).isEmpty();
   }
 
   @Test
-  public void validatorBehaviorWhenCommitterConfigEmpty() {
-    assertThat(ChangeEmailValidator.isCommitterActive(EMPTY_PLUGIN_CONFIG)).isFalse();
+  public void validatorBehaviorWhenAuthorRejectListConfigEmpty() {
+    assertThat(ChangeEmailValidator.isAuthorRejectListActive(EMPTY_PLUGIN_CONFIG)).isFalse();
+    assertThat(ChangeEmailValidator.getRejectedAuthorEmailPatterns(EMPTY_PLUGIN_CONFIG)).isEmpty();
+  }
+
+  @Test
+  public void validatorBehaviorWhenCommitterAllowListConfigEmpty() {
+    assertThat(ChangeEmailValidator.isCommitterAllowListActive(EMPTY_PLUGIN_CONFIG)).isFalse();
     assertThat(ChangeEmailValidator.getAllowedCommitterEmailPatterns(EMPTY_PLUGIN_CONFIG))
         .isEmpty();
   }
+
+  @Test
+  public void validatorBehaviorWhenCommitterRejectListConfigEmpty() {
+    assertThat(ChangeEmailValidator.isCommitterRejectListActive(EMPTY_PLUGIN_CONFIG)).isFalse();
+    assertThat(ChangeEmailValidator.getRejectedCommitterEmailPatterns(EMPTY_PLUGIN_CONFIG))
+        .isEmpty();
+  }
 }