Adds Change Email Uploadvalidator

This adds the ability to define patterns that will allow Author and
Committer emails that match to push changes. All others are rejected.
This check uses 'java.util.regex.Pattern'.

Author and Committer will have separate pattern lists, but they follow
all the same rules when checking against their lists for validation.

Defining the list of patterns is done on project level in the
project.config file of the project. Multiple entries can be defined
for both Author and Committer and usage the can be mixed. Usage being
specific email or regular expression.

Change-Id: I27c9398994e1ee6bb1c6553a3144e8d998f2b020
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ChangeEmailValidator.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ChangeEmailValidator.java
new file mode 100644
index 0000000..6deb42c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ChangeEmailValidator.java
@@ -0,0 +1,155 @@
+// 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.googlesource.gerrit.plugins.uploadvalidator;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.config.ProjectConfigEntry;
+import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidationListener;
+import com.google.gerrit.server.git.validators.CommitValidationMessage;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.regex.Pattern;
+
+public class ChangeEmailValidator implements CommitValidationListener {
+  public static AbstractModule module() {
+    return new AbstractModule() {
+      @Override
+      public void configure() {
+        DynamicSet.bind(binder(), CommitValidationListener.class).to(ChangeEmailValidator.class);
+        bind(ProjectConfigEntry.class)
+            .annotatedWith(Exports.named(KEY_ALLOWED_AUTHOR_EMAIL_PATTERN))
+            .toInstance(
+                new ProjectConfigEntry(
+                    "Author Email Pattern",
+                    null,
+                    ProjectConfigEntryType.ARRAY,
+                    null,
+                    false,
+                    "Commits with author email not matching one of these pattterns will be rejected."));
+        bind(ProjectConfigEntry.class)
+            .annotatedWith(Exports.named(KEY_ALLOWED_COMMITTER_EMAIL_PATTERN))
+            .toInstance(
+                new ProjectConfigEntry(
+                    "Committer Email Pattern",
+                    null,
+                    ProjectConfigEntryType.ARRAY,
+                    null,
+                    false,
+                    "Commits with committer email not matching one of these patterns will be rejected."));
+      }
+    };
+  }
+
+  public static final String KEY_ALLOWED_AUTHOR_EMAIL_PATTERN = "allowedAuthorEmailPattern";
+  public static final String KEY_ALLOWED_COMMITTER_EMAIL_PATTERN = "allowedCommitterEmailPattern";
+  private final String pluginName;
+  private final PluginConfigFactory cfgFactory;
+  private final GitRepositoryManager repoManager;
+  private final ValidatorConfig validatorConfig;
+
+  @Inject
+  ChangeEmailValidator(
+      @PluginName String pluginName,
+      PluginConfigFactory cfgFactory,
+      GitRepositoryManager repoManager,
+      ValidatorConfig validatorConfig) {
+    this.pluginName = pluginName;
+    this.cfgFactory = cfgFactory;
+    this.repoManager = repoManager;
+    this.validatorConfig = validatorConfig;
+  }
+
+  @VisibleForTesting
+  static String[] getAllowedAuthorEmailPatterns(PluginConfig cfg) {
+    return cfg.getStringList(KEY_ALLOWED_AUTHOR_EMAIL_PATTERN);
+  }
+
+  @VisibleForTesting
+  static String[] getAllowedCommitterEmailPatterns(PluginConfig cfg) {
+    return cfg.getStringList(KEY_ALLOWED_COMMITTER_EMAIL_PATTERN);
+  }
+
+  @VisibleForTesting
+  static boolean isAuthorActive(PluginConfig cfg) {
+    return cfg.getStringList(KEY_ALLOWED_AUTHOR_EMAIL_PATTERN).length > 0;
+  }
+
+  @VisibleForTesting
+  static boolean isCommitterActive(PluginConfig cfg) {
+    return cfg.getStringList(KEY_ALLOWED_COMMITTER_EMAIL_PATTERN).length > 0;
+  }
+
+  @Override
+  public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
+      throws CommitValidationException {
+    try {
+      PluginConfig cfg =
+          cfgFactory.getFromProjectConfigWithInheritance(
+              receiveEvent.project.getNameKey(), pluginName);
+      if (isAuthorActive(cfg)
+          && validatorConfig.isEnabledForRef(
+              receiveEvent.user,
+              receiveEvent.getProjectNameKey(),
+              receiveEvent.getRefName(),
+              KEY_ALLOWED_AUTHOR_EMAIL_PATTERN)) {
+        if (!performValidation(
+            receiveEvent.commit.getAuthorIdent().getEmailAddress(),
+            getAllowedAuthorEmailPatterns(cfg))) {
+          throw new CommitValidationException(
+              "Author Email <"
+                  + receiveEvent.commit.getAuthorIdent().getEmailAddress()
+                  + "> - is not allowed for this Project.");
+        }
+      }
+      if (isCommitterActive(cfg)
+          && validatorConfig.isEnabledForRef(
+              receiveEvent.user,
+              receiveEvent.getProjectNameKey(),
+              receiveEvent.getRefName(),
+              KEY_ALLOWED_COMMITTER_EMAIL_PATTERN)) {
+        if (!performValidation(
+            receiveEvent.commit.getCommitterIdent().getEmailAddress(),
+            getAllowedCommitterEmailPatterns(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);
+    }
+    return Collections.emptyList();
+  }
+
+  @VisibleForTesting
+  static boolean performValidation(String email, String[] allowedEmailPatterns) {
+    return Arrays.stream(allowedEmailPatterns)
+        .anyMatch(s -> Pattern.matches(s, Strings.nullToEmpty(email)));
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/EmailDomainWhitelistValidator.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/EmailDomainWhitelistValidator.java
deleted file mode 100644
index 1dd4b40..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/EmailDomainWhitelistValidator.java
+++ /dev/null
@@ -1,122 +0,0 @@
-// 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.googlesource.gerrit.plugins.uploadvalidator;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Strings;
-import com.google.gerrit.extensions.annotations.Exports;
-import com.google.gerrit.extensions.annotations.PluginName;
-import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.server.config.PluginConfig;
-import com.google.gerrit.server.config.PluginConfigFactory;
-import com.google.gerrit.server.config.ProjectConfigEntry;
-import com.google.gerrit.server.events.CommitReceivedEvent;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.validators.CommitValidationException;
-import com.google.gerrit.server.git.validators.CommitValidationListener;
-import com.google.gerrit.server.git.validators.CommitValidationMessage;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.inject.AbstractModule;
-import com.google.inject.Inject;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.regex.Pattern;
-
-public class EmailDomainWhitelistValidator implements CommitValidationListener {
-
-  public static AbstractModule module() {
-    return new AbstractModule() {
-      @Override
-      public void configure() {
-        DynamicSet.bind(binder(), CommitValidationListener.class)
-            .to(EmailDomainWhitelistValidator.class);
-        bind(ProjectConfigEntry.class)
-            .annotatedWith(Exports.named(KEY_EMAIL_DOMAIN_WHITELIST))
-            .toInstance(
-                new ProjectConfigEntry(
-                    "Email Domain White List",
-                    null,
-                    ProjectConfigEntryType.ARRAY,
-                    null,
-                    false,
-                    "Whitelist of email domains that will be permitted."));
-      }
-    };
-  }
-
-  public static final String KEY_EMAIL_DOMAIN_WHITELIST = "emailDomainWhitelist";
-
-  private final String pluginName;
-  private final PluginConfigFactory cfgFactory;
-  private final GitRepositoryManager repoManager;
-  private final ValidatorConfig validatorConfig;
-
-  @Inject
-  EmailDomainWhitelistValidator(
-      @PluginName String pluginName,
-      PluginConfigFactory cfgFactory,
-      GitRepositoryManager repoManager,
-      ValidatorConfig validatorConfig) {
-    this.pluginName = pluginName;
-    this.cfgFactory = cfgFactory;
-    this.repoManager = repoManager;
-    this.validatorConfig = validatorConfig;
-  }
-
-  @VisibleForTesting
-  static String[] getEmailDomainWhiteList(PluginConfig cfg) {
-    return cfg.getStringList(KEY_EMAIL_DOMAIN_WHITELIST);
-  }
-
-  @VisibleForTesting
-  static boolean isActive(PluginConfig cfg) {
-    return cfg.getStringList(KEY_EMAIL_DOMAIN_WHITELIST).length > 0;
-  }
-
-  @Override
-  public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
-      throws CommitValidationException {
-    try {
-      PluginConfig cfg =
-          cfgFactory.getFromProjectConfigWithInheritance(
-              receiveEvent.project.getNameKey(), pluginName);
-      if (isActive(cfg)
-          && validatorConfig.isEnabledForRef(
-              receiveEvent.user,
-              receiveEvent.getProjectNameKey(),
-              receiveEvent.getRefName(),
-              KEY_EMAIL_DOMAIN_WHITELIST)) {
-        if (!performValidation(
-            receiveEvent.user.getAccount().getPreferredEmail(), getEmailDomainWhiteList(cfg))) {
-          throw new CommitValidationException(
-              "Email <"
-                  + receiveEvent.user.getAccount().getPreferredEmail()
-                  + "> - is not whitelisted for this Project.");
-        }
-      }
-    } catch (NoSuchProjectException e) {
-      throw new CommitValidationException("Failed to check for Email Domain Whitelist ", e);
-    }
-    return Collections.emptyList();
-  }
-
-  @VisibleForTesting
-  static boolean performValidation(String email, String[] emailDomainWhitelist) {
-
-    return Arrays.stream(emailDomainWhitelist)
-        .anyMatch(s -> Pattern.matches(s, Strings.nullToEmpty(email)));
-  }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/Module.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/Module.java
index b116f80..e15ecb2 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/Module.java
@@ -27,7 +27,7 @@
     install(FooterValidator.module());
     install(MaxPathLengthValidator.module());
     install(FileExtensionValidator.module());
-    install(EmailDomainWhitelistValidator.module());
+    install(ChangeEmailValidator.module());
     install(InvalidFilenameValidator.module());
     install(SubmoduleValidator.module());
     install(SymlinkValidator.module());
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index a200dff..67da3ad 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -12,6 +12,6 @@
 - reject submodules
 - required footers
 - maximum allowed path length
-- whitelisting email domains
+- allowing committer and author email addresses
 
 Pushes of commits that violate these settings are rejected by Gerrit.
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 2572308..95c6775 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -32,17 +32,37 @@
     rejectSubmodule = false
     rejectDuplicatePathnames = false
     rejectDuplicatePathnamesLocale = en
-    emailDomainWhitelist = .*@example.com$
-    emailDomainWhitelist = .*gerrit.*
+    allowedAuthorEmailPattern = .*@example\\.com$
+    allowedAuthorEmailPattern = admin@example\\.com
+    allowedCommitterEmailPattern = .*gerrit\\.com
+    allowedCommitterEmailPattern =  admin@gerrit\\..*
 ```
 
-plugin.@PLUGIN@.emailDomainWhitelist
-:    Email Domain to Whitelist.
+plugin.@PLUGIN@.allowedAuthorEmailPattern
+:    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.
-    The emails may be configured using specific emails, patterns, or
-    regular expressions.
+
+    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.
+
+    This check is using `java.util.regex.Pattern` which is described
+    [here][1].
+
+plugin.@PLUGIN@.allowedCommitterEmailPattern
+:    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.
+
+    This check is using `java.util.regex.Pattern` which is described
+    [here][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
new file mode 100644
index 0000000..0b208f2
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/ChangeEmailTest.java
@@ -0,0 +1,81 @@
+// 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.googlesource.gerrit.plugins.uploadvalidator;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.googlesource.gerrit.plugins.uploadvalidator.TestUtils.EMPTY_PLUGIN_CONFIG;
+
+import org.junit.Test;
+
+public class ChangeEmailTest {
+  private static final String[] allowedEmailPatterns = {
+    ".*@example\\.com.*",
+    "testing\\.com",
+    "tester@testing\\.com",
+    ".*google\\.com",
+    "tester@gerrit\\..*"
+  };
+
+  @Test
+  public void testEmailValid() throws Exception {
+    assertThat(
+            ChangeEmailValidator.performValidation("tester@example.com.net", allowedEmailPatterns))
+        .isTrue();
+    assertThat(ChangeEmailValidator.performValidation("tester@testing.com", allowedEmailPatterns))
+        .isTrue();
+    assertThat(ChangeEmailValidator.performValidation("tester@google.com", allowedEmailPatterns))
+        .isTrue();
+    assertThat(ChangeEmailValidator.performValidation("tester@gerrit.net", allowedEmailPatterns))
+        .isTrue();
+  }
+
+  @Test
+  public void testEmailInvalid() throws Exception {
+    assertThat(ChangeEmailValidator.performValidation("tester@example.org", allowedEmailPatterns))
+        .isFalse();
+    assertThat(ChangeEmailValidator.performValidation("test@testing.com", allowedEmailPatterns))
+        .isFalse();
+    assertThat(
+            ChangeEmailValidator.performValidation("tester@google.com.net", allowedEmailPatterns))
+        .isFalse();
+    assertThat(
+            ChangeEmailValidator.performValidation(
+                "emailtester@gerritnet.com", allowedEmailPatterns))
+        .isFalse();
+  }
+
+  @Test
+  public void testEmailNull() throws Exception {
+    assertThat(ChangeEmailValidator.performValidation(null, allowedEmailPatterns)).isFalse();
+  }
+
+  @Test
+  public void testEmailEmpty() throws Exception {
+    assertThat(ChangeEmailValidator.performValidation("", allowedEmailPatterns)).isFalse();
+  }
+
+  @Test
+  public void validatorBehaviorWhenAuthorConfigEmpty() {
+    assertThat(ChangeEmailValidator.isAuthorActive(EMPTY_PLUGIN_CONFIG)).isFalse();
+    assertThat(ChangeEmailValidator.getAllowedAuthorEmailPatterns(EMPTY_PLUGIN_CONFIG)).isEmpty();
+  }
+
+  @Test
+  public void validatorBehaviorWhenCommitterConfigEmpty() {
+    assertThat(ChangeEmailValidator.isCommitterActive(EMPTY_PLUGIN_CONFIG)).isFalse();
+    assertThat(ChangeEmailValidator.getAllowedCommitterEmailPatterns(EMPTY_PLUGIN_CONFIG))
+        .isEmpty();
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/EmailDomainWhitelistTest.java b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/EmailDomainWhitelistTest.java
deleted file mode 100644
index 364f047..0000000
--- a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/EmailDomainWhitelistTest.java
+++ /dev/null
@@ -1,89 +0,0 @@
-// 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.googlesource.gerrit.plugins.uploadvalidator;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.googlesource.gerrit.plugins.uploadvalidator.TestUtils.EMPTY_PLUGIN_CONFIG;
-
-import org.junit.Test;
-
-public class EmailDomainWhitelistTest {
-  private static final String[] emailDomainWhitelist = {
-    ".*@example.com$", "testing.com", "tester@testing.com"
-  };
-
-  private static final String emailValidListed01 = "test@example.com";
-  private static final String emailValidListed02 = "tester@testing.com";
-  private static final String emailValidNotListed01 = "test@android.com";
-  private static final String emailValidNotListed02 = "test@testing.com";
-  private static final String emailInvalidNotListed = "email[/]example?test.com";
-  private static final String emailNullNotListed = null;
-  private static final String emailEmptyNotListed = "";
-
-  @Test
-  public void testEmailValidListed() throws Exception {
-    assertThat(
-            EmailDomainWhitelistValidator.performValidation(
-                emailValidListed01, emailDomainWhitelist))
-        .isTrue();
-    assertThat(
-            EmailDomainWhitelistValidator.performValidation(
-                emailValidListed02, emailDomainWhitelist))
-        .isTrue();
-  }
-
-  @Test
-  public void testEmailValidNotListed() throws Exception {
-    assertThat(
-            EmailDomainWhitelistValidator.performValidation(
-                emailValidNotListed01, emailDomainWhitelist))
-        .isFalse();
-    assertThat(
-            EmailDomainWhitelistValidator.performValidation(
-                emailValidNotListed02, emailDomainWhitelist))
-        .isFalse();
-  }
-
-  @Test
-  public void testEmailInvalidNotListed() throws Exception {
-    assertThat(
-            EmailDomainWhitelistValidator.performValidation(
-                emailInvalidNotListed, emailDomainWhitelist))
-        .isFalse();
-  }
-
-  @Test
-  public void testEmailNullNotListed() throws Exception {
-    assertThat(
-            EmailDomainWhitelistValidator.performValidation(
-                emailNullNotListed, emailDomainWhitelist))
-        .isFalse();
-  }
-
-  @Test
-  public void testEmailEmptyNotListed() throws Exception {
-    assertThat(
-            EmailDomainWhitelistValidator.performValidation(
-                emailEmptyNotListed, emailDomainWhitelist))
-        .isFalse();
-  }
-
-  @Test
-  public void validatorBehaviorWhenConfigEmpty() {
-    assertThat(EmailDomainWhitelistValidator.isActive(EMPTY_PLUGIN_CONFIG)).isFalse();
-    assertThat(EmailDomainWhitelistValidator.getEmailDomainWhiteList(EMPTY_PLUGIN_CONFIG))
-        .isEmpty();
-  }
-}