Add push option to skip rules validation

Validation rules may block commits that should be uploaded regardless of
the rules (e.g. false positives, exceptions, etc).

This patch provides Gerrit administrators ability to allow their
uploaders to bypass all validation rules by using git push option
"uploadvalidator~skip". This feature is controlled by uploadvalidator
config "skipViaPushOption".

Bug: Issue: 14362
Change-Id: I4305ea8a0923e531742ee77d8bd9d394099e2eb9
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/BlockedKeywordValidator.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/BlockedKeywordValidator.java
index 288afcf..8b8b244 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/BlockedKeywordValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/BlockedKeywordValidator.java
@@ -21,6 +21,7 @@
 import com.google.common.cache.LoadingCache;
 import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
@@ -149,7 +150,8 @@
               receiveEvent.user,
               receiveEvent.getProjectNameKey(),
               receiveEvent.getRefName(),
-              KEY_CHECK_BLOCKED_KEYWORD)) {
+              KEY_CHECK_BLOCKED_KEYWORD,
+              receiveEvent.pushOptions)) {
         ImmutableMap<String, Pattern> blockedKeywordPatterns =
             patternCache.getAll(
                 Arrays.asList(cfg.getStringList(KEY_CHECK_BLOCKED_KEYWORD_PATTERN)));
@@ -181,7 +183,11 @@
       PluginConfig cfg = cfgFactory.getFromProjectConfigWithInheritance(projectNameKey, pluginName);
       if (isActive(cfg)
           && validatorConfig.isEnabled(
-              null, projectNameKey, "", KEY_CHECK_COMMENT_BLOCKED_KEYWORD)) {
+              null,
+              projectNameKey,
+              "",
+              KEY_CHECK_COMMENT_BLOCKED_KEYWORD,
+              ImmutableListMultimap.of())) {
         ImmutableMap<String, Pattern> blockedKeywordPatterns =
             patternCache.getAll(
                 Arrays.asList(cfg.getStringList(KEY_CHECK_BLOCKED_KEYWORD_PATTERN)));
@@ -221,15 +227,18 @@
   }
 
   private static Optional<CommentValidationFailure> validateComment(
-      ImmutableMap<String, Pattern>  blockedKeywordPatterns, CommentForValidation comment) {
+      ImmutableMap<String, Pattern> blockedKeywordPatterns, CommentForValidation comment) {
     // Uses HashSet data structure for de-duping found blocked keywords.
-    Set<String> findings = new LinkedHashSet<String>(
-        findBlockedKeywordsInString(blockedKeywordPatterns.values(), comment.getText()));
+    Set<String> findings =
+        new LinkedHashSet<String>(
+            findBlockedKeywordsInString(blockedKeywordPatterns.values(), comment.getText()));
     if (findings.isEmpty()) {
       return Optional.empty();
     }
-    return Optional.of(comment.failValidation(
-        String.format("banned words found in your comment (%s)", Iterables.toString(findings))));
+    return Optional.of(
+        comment.failValidation(
+            String.format(
+                "banned words found in your comment (%s)", Iterables.toString(findings))));
   }
 
   private static void checkCommitMessageForBlockedKeywords(
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 04525ec..b872620 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ChangeEmailValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ChangeEmailValidator.java
@@ -49,7 +49,8 @@
                     ProjectConfigEntryType.ARRAY,
                     null,
                     false,
-                    "Commits with author email not matching one of these pattterns will be rejected."));
+                    "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(
@@ -59,7 +60,8 @@
                     ProjectConfigEntryType.ARRAY,
                     null,
                     false,
-                    "Commits with committer email not matching one of these patterns will be rejected."));
+                    "Commits with committer email not matching one of these patterns will be"
+                        + " rejected."));
       }
     };
   }
@@ -112,7 +114,8 @@
               receiveEvent.user,
               receiveEvent.getProjectNameKey(),
               receiveEvent.getRefName(),
-              KEY_ALLOWED_AUTHOR_EMAIL_PATTERN)) {
+              KEY_ALLOWED_AUTHOR_EMAIL_PATTERN,
+              receiveEvent.pushOptions)) {
         if (!performValidation(
             receiveEvent.commit.getAuthorIdent().getEmailAddress(),
             getAllowedAuthorEmailPatterns(cfg))) {
@@ -127,7 +130,8 @@
               receiveEvent.user,
               receiveEvent.getProjectNameKey(),
               receiveEvent.getRefName(),
-              KEY_ALLOWED_COMMITTER_EMAIL_PATTERN)) {
+              KEY_ALLOWED_COMMITTER_EMAIL_PATTERN,
+              receiveEvent.pushOptions)) {
         if (!performValidation(
             receiveEvent.commit.getCommitterIdent().getEmailAddress(),
             getAllowedCommitterEmailPatterns(cfg))) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ContentTypeValidator.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ContentTypeValidator.java
index df1b633..3044ccd 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ContentTypeValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ContentTypeValidator.java
@@ -127,7 +127,8 @@
               receiveEvent.user,
               receiveEvent.getProjectNameKey(),
               receiveEvent.getRefName(),
-              KEY_BLOCKED_CONTENT_TYPE)) {
+              KEY_BLOCKED_CONTENT_TYPE,
+              receiveEvent.pushOptions)) {
         try (Repository repo = repoManager.openRepository(receiveEvent.project.getNameKey())) {
           List<CommitValidationMessage> messages =
               performValidation(
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/DuplicatePathnameValidator.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/DuplicatePathnameValidator.java
index 8a9ea2b..d8dd451 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/DuplicatePathnameValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/DuplicatePathnameValidator.java
@@ -185,7 +185,8 @@
               receiveEvent.user,
               receiveEvent.getProjectNameKey(),
               receiveEvent.getRefName(),
-              KEY_REJECT_DUPLICATE_PATHNAMES)) {
+              KEY_REJECT_DUPLICATE_PATHNAMES,
+              receiveEvent.pushOptions)) {
         locale = getLocale(cfg);
         try (Repository repo = repoManager.openRepository(receiveEvent.project.getNameKey())) {
           List<CommitValidationMessage> messages =
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/FileExtensionValidator.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/FileExtensionValidator.java
index 4b29b1f..5fc1657 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/FileExtensionValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/FileExtensionValidator.java
@@ -104,7 +104,8 @@
               receiveEvent.user,
               receiveEvent.getProjectNameKey(),
               receiveEvent.getRefName(),
-              KEY_BLOCKED_FILE_EXTENSION)) {
+              KEY_BLOCKED_FILE_EXTENSION,
+              receiveEvent.pushOptions)) {
         try (Repository repo = repoManager.openRepository(receiveEvent.project.getNameKey())) {
           List<CommitValidationMessage> messages =
               performValidation(
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/FooterValidator.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/FooterValidator.java
index b5b92c2..67d49a6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/FooterValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/FooterValidator.java
@@ -89,7 +89,8 @@
               receiveEvent.user,
               receiveEvent.getProjectNameKey(),
               receiveEvent.getRefName(),
-              KEY_REQUIRED_FOOTER)) {
+              KEY_REQUIRED_FOOTER,
+              receiveEvent.pushOptions)) {
         List<CommitValidationMessage> messages = new LinkedList<>();
         Set<String> footers =
             FluentIterable.from(receiveEvent.commit.getFooterLines())
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/InvalidFilenameValidator.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/InvalidFilenameValidator.java
index c6371df..f624830 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/InvalidFilenameValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/InvalidFilenameValidator.java
@@ -99,7 +99,8 @@
               receiveEvent.user,
               receiveEvent.getProjectNameKey(),
               receiveEvent.getRefName(),
-              KEY_INVALID_FILENAME)) {
+              KEY_INVALID_FILENAME,
+              receiveEvent.pushOptions)) {
         try (Repository repo = repoManager.openRepository(receiveEvent.project.getNameKey())) {
           List<CommitValidationMessage> messages =
               performValidation(
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/InvalidLineEndingValidator.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/InvalidLineEndingValidator.java
index aa72ee0..4bffce6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/InvalidLineEndingValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/InvalidLineEndingValidator.java
@@ -109,7 +109,8 @@
               receiveEvent.user,
               receiveEvent.getProjectNameKey(),
               receiveEvent.getRefName(),
-              KEY_CHECK_REJECT_WINDOWS_LINE_ENDINGS)) {
+              KEY_CHECK_REJECT_WINDOWS_LINE_ENDINGS,
+              receiveEvent.pushOptions)) {
         try (Repository repo = repoManager.openRepository(receiveEvent.project.getNameKey())) {
           List<CommitValidationMessage> messages =
               performValidation(repo, receiveEvent.commit, receiveEvent.revWalk, cfg);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/MaxPathLengthValidator.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/MaxPathLengthValidator.java
index a3db8f4..6183317 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/MaxPathLengthValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/MaxPathLengthValidator.java
@@ -93,7 +93,8 @@
               receiveEvent.user,
               receiveEvent.getProjectNameKey(),
               receiveEvent.getRefName(),
-              KEY_MAX_PATH_LENGTH)) {
+              KEY_MAX_PATH_LENGTH,
+              receiveEvent.pushOptions)) {
         int maxPathLength = cfg.getInt(KEY_MAX_PATH_LENGTH, 0);
         try (Repository repo = repoManager.openRepository(receiveEvent.project.getNameKey())) {
           List<CommitValidationMessage> messages =
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 e15ecb2..98261ea 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/Module.java
@@ -14,6 +14,8 @@
 
 package com.googlesource.gerrit.plugins.uploadvalidator;
 
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.git.receive.PluginPushOption;
 import com.google.inject.AbstractModule;
 import com.google.inject.Scopes;
 
@@ -38,5 +40,7 @@
     install(ValidatorConfig.module());
 
     bind(ConfigFactory.class).to(PluginConfigWithInheritanceFactory.class).in(Scopes.SINGLETON);
+
+    DynamicSet.bind(binder(), PluginPushOption.class).to(SkipValidationPushOption.class);
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/SkipValidationPushOption.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/SkipValidationPushOption.java
new file mode 100644
index 0000000..b690bd4
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/SkipValidationPushOption.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2021 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.gerrit.server.git.receive.PluginPushOption;
+
+/** Push option that allows to skip the uploadvalidator plugin validation. */
+public final class SkipValidationPushOption implements PluginPushOption {
+  public static final String NAME = "skip";
+
+  public String getName() {
+    return NAME;
+  }
+
+  public String getDescription() {
+    return "skip uploadvalidator validation";
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/SubmoduleValidator.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/SubmoduleValidator.java
index f96d325..99e2641 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/SubmoduleValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/SubmoduleValidator.java
@@ -96,7 +96,8 @@
               receiveEvent.user,
               receiveEvent.getProjectNameKey(),
               receiveEvent.getRefName(),
-              KEY_CHECK_SUBMODULE)) {
+              KEY_CHECK_SUBMODULE,
+              receiveEvent.pushOptions)) {
         try (Repository repo = repoManager.openRepository(receiveEvent.project.getNameKey())) {
           List<CommitValidationMessage> messages =
               performValidation(repo, receiveEvent.commit, receiveEvent.revWalk);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/SymlinkValidator.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/SymlinkValidator.java
index 85a8151..31faa75 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/SymlinkValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/SymlinkValidator.java
@@ -97,7 +97,8 @@
               receiveEvent.user,
               receiveEvent.getProjectNameKey(),
               receiveEvent.getRefName(),
-              KEY_CHECK_SYMLINK)) {
+              KEY_CHECK_SYMLINK,
+              receiveEvent.pushOptions)) {
         try (Repository repo = repoManager.openRepository(receiveEvent.project.getNameKey())) {
           List<CommitValidationMessage> messages =
               performValidation(repo, receiveEvent.commit, receiveEvent.revWalk);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ValidatorConfig.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ValidatorConfig.java
index 38d7a4e..d7cca55 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ValidatorConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ValidatorConfig.java
@@ -17,6 +17,7 @@
 import static com.google.common.collect.ImmutableList.toImmutableList;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.AccessSection;
 import com.google.gerrit.entities.AccountGroup;
@@ -25,6 +26,7 @@
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 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.server.IdentifiedUser;
 import com.google.gerrit.server.config.PluginConfig;
@@ -44,6 +46,7 @@
   private static final Logger log = LoggerFactory.getLogger(ValidatorConfig.class);
   private static final String KEY_PROJECT = "project";
   private static final String KEY_REF = "ref";
+  private final String pluginName;
   private final ConfigFactory configFactory;
   private final GroupByNameFinder groupByNameFinder;
 
@@ -77,7 +80,11 @@
   }
 
   @Inject
-  public ValidatorConfig(ConfigFactory configFactory, GroupByNameFinder groupByNameFinder) {
+  public ValidatorConfig(
+      @PluginName String pluginName,
+      ConfigFactory configFactory,
+      GroupByNameFinder groupByNameFinder) {
+    this.pluginName = pluginName;
     this.configFactory = configFactory;
     this.groupByNameFinder = groupByNameFinder;
   }
@@ -96,11 +103,13 @@
       @Nullable IdentifiedUser user,
       Project.NameKey projectName,
       String refName,
-      String validatorOp) {
+      String validatorOp,
+      ImmutableListMultimap<String, String> pushOptions) {
     PluginConfig conf = configFactory.get(projectName);
 
     return conf != null
         && isValidConfig(conf, projectName)
+        && !isDisabledByPushOption(conf, pushOptions)
         && activeForRef(conf, refName)
         && (user == null || activeForEmail(conf, user.getAccount().preferredEmail()))
         && activeForGroup(conf, user)
@@ -142,6 +151,15 @@
     return Arrays.asList(c).contains(validatorOp);
   }
 
+  private boolean isDisabledByPushOption(
+      PluginConfig config, ImmutableListMultimap<String, String> pushOptions) {
+    String qualifiedName = pluginName + "~" + SkipValidationPushOption.NAME;
+    if (!config.getBoolean("skipViaPushOption", false)) {
+      return false;
+    }
+    return pushOptions.containsKey(qualifiedName);
+  }
+
   private boolean activeForProject(PluginConfig config, String project) {
     return matchCriteria(config, "project", project, true, false);
   }
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 56d6296..7c1cc23 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -418,3 +418,12 @@
     skipGroup = ldap/GerritAdmins
     skipRef = refs/heads/master
 ```
+
+plugin.@PLUGIN@.skipViaPushOption
+:   Allow all users to skip validation using push options
+
+    This check allows all users to bypass all validation rules if they set push
+    option "@PLUGIN@~skip" (e.g. git push -o "@PLUGIN@~skip"), regardless of
+    other skip rules.
+
+    Default: false
diff --git a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/EmailAwareValidatorConfigTest.java b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/EmailAwareValidatorConfigTest.java
index 8c00744..88ce317 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/EmailAwareValidatorConfigTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/EmailAwareValidatorConfigTest.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.collect.ImmutableListMultimap;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.IdentifiedUser;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -30,7 +31,9 @@
     ValidatorConfig config =
         getConfig("[plugin \"uploadvalidator\"]\n" + "blockedFileExtension = jar");
 
-    assertThat(config.isEnabled(anyUser, projectName, "anyRef", "blockedFileExtension"))
+    assertThat(
+            config.isEnabled(
+                anyUser, projectName, "anyRef", "blockedFileExtension", ImmutableListMultimap.of()))
         .isTrue();
   }
 
@@ -40,7 +43,13 @@
     ValidatorConfig config =
         getConfig("[plugin \"uploadvalidator\"]\n" + "blockedFileExtension = jar");
 
-    assertThat(config.isEnabled(missingEmail, projectName, "anyRef", "blockedFileExtension"))
+    assertThat(
+            config.isEnabled(
+                missingEmail,
+                projectName,
+                "anyRef",
+                "blockedFileExtension",
+                ImmutableListMultimap.of()))
         .isTrue();
   }
 
@@ -56,7 +65,11 @@
 
     assertThat(
             config.isEnabled(
-                anyUser, projectName, "refs/heads/anyref", "blockedFileExtension"))
+                anyUser,
+                projectName,
+                "refs/heads/anyref",
+                "blockedFileExtension",
+                ImmutableListMultimap.of()))
         .isTrue();
   }
 
@@ -70,7 +83,11 @@
 
     assertThat(
             config.isEnabled(
-                anyUser, projectName, "refs/heads/anyref", "blockedFileExtension"))
+                anyUser,
+                projectName,
+                "refs/heads/anyref",
+                "blockedFileExtension",
+                ImmutableListMultimap.of()))
         .isFalse();
   }
 
@@ -85,11 +102,19 @@
 
     assertThat(
             config.isEnabled(
-                anyUser, projectName, "refs/heads/anyref", "blockedFileExtension"))
+                anyUser,
+                projectName,
+                "refs/heads/anyref",
+                "blockedFileExtension",
+                ImmutableListMultimap.of()))
         .isFalse();
     assertThat(
             config.isEnabled(
-                exampleOrgUser, projectName, "refs/heads/anyref", "blockedFileExtension"))
+                exampleOrgUser,
+                projectName,
+                "refs/heads/anyref",
+                "blockedFileExtension",
+                ImmutableListMultimap.of()))
         .isTrue();
   }
 
@@ -104,7 +129,11 @@
 
     assertThat(
             config.isEnabled(
-                missingEmail, projectName, "refs/heads/anyref", "blockedFileExtension"))
+                missingEmail,
+                projectName,
+                "refs/heads/anyref",
+                "blockedFileExtension",
+                ImmutableListMultimap.of()))
         .isFalse();
   }
 
@@ -121,19 +150,34 @@
 
     assertThat(
             config.isEnabled(
-                exampleOrgUser, projectName, "refs/heads/anyref", "blockedFileExtension"))
-        .isTrue();
-    assertThat(
-            config.isEnabled(xUser, projectName, "refs/heads/anyref", "blockedFileExtension"))
+                exampleOrgUser,
+                projectName,
+                "refs/heads/anyref",
+                "blockedFileExtension",
+                ImmutableListMultimap.of()))
         .isTrue();
     assertThat(
             config.isEnabled(
-                anyUser, projectName, "refs/heads/anyref", "blockedFileExtension"))
+                xUser,
+                projectName,
+                "refs/heads/anyref",
+                "blockedFileExtension",
+                ImmutableListMultimap.of()))
+        .isTrue();
+    assertThat(
+            config.isEnabled(
+                anyUser,
+                projectName,
+                "refs/heads/anyref",
+                "blockedFileExtension",
+                ImmutableListMultimap.of()))
         .isFalse();
   }
 
   private ValidatorConfig getConfig(String defaultConfig) throws ConfigInvalidException {
     return new ValidatorConfig(
-        new FakeConfigFactory(projectName, defaultConfig), new FakeGroupByNameFinder());
+        "uploadvalidator",
+        new FakeConfigFactory(projectName, defaultConfig),
+        new FakeGroupByNameFinder());
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/GroupAwareValidatorConfigTest.java b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/GroupAwareValidatorConfigTest.java
index 82ceced..c78b64b 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/GroupAwareValidatorConfigTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/GroupAwareValidatorConfigTest.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.collect.ImmutableListMultimap;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.util.time.TimeUtil;
@@ -23,6 +24,7 @@
 
 public class GroupAwareValidatorConfigTest {
   private Project.NameKey projectName = Project.nameKey("testProject");
+  private static final String pluginName = "uploadvalidator";
 
   @Test
   public void isEnabledForNoGroupsByDefault() throws Exception {
@@ -30,11 +32,15 @@
 
     ValidatorConfig validatorConfig =
         new ValidatorConfig(
-            new FakeConfigFactory(projectName, config), new FakeGroupByNameFinder());
+            pluginName, new FakeConfigFactory(projectName, config), new FakeGroupByNameFinder());
 
     assertThat(
             validatorConfig.isEnabled(
-                new FakeUserProvider().get(), projectName, "anyRef", "blockedFileExtension"))
+                new FakeUserProvider().get(),
+                projectName,
+                "anyRef",
+                "blockedFileExtension",
+                ImmutableListMultimap.of()))
         .isTrue();
   }
 
@@ -48,14 +54,15 @@
 
     ValidatorConfig validatorConfig =
         new ValidatorConfig(
-            new FakeConfigFactory(projectName, config), new FakeGroupByNameFinder());
+            pluginName, new FakeConfigFactory(projectName, config), new FakeGroupByNameFinder());
 
     assertThat(
             validatorConfig.isEnabled(
                 new FakeUserProvider("fooGroup", "bazGroup").get(),
                 projectName,
                 "anyRef",
-                "blockedFileExtension"))
+                "blockedFileExtension",
+                ImmutableListMultimap.of()))
         .isTrue();
   }
 
@@ -66,6 +73,7 @@
 
     ValidatorConfig validatorConfig =
         new ValidatorConfig(
+            pluginName,
             new FakeConfigFactory(projectName, config),
             new FakeGroupByNameFinder(
                 AccountGroup.nameKey("testGroupName"),
@@ -78,7 +86,8 @@
                 new FakeUserProvider("testGroupId").get(),
                 projectName,
                 "anyRef",
-                "blockedFileExtension"))
+                "blockedFileExtension",
+                ImmutableListMultimap.of()))
         .isTrue();
   }
 
@@ -92,14 +101,15 @@
 
     ValidatorConfig validatorConfig =
         new ValidatorConfig(
-            new FakeConfigFactory(projectName, config), new FakeGroupByNameFinder());
+            pluginName, new FakeConfigFactory(projectName, config), new FakeGroupByNameFinder());
 
     assertThat(
             validatorConfig.isEnabled(
                 new FakeUserProvider("bazGroup").get(),
                 projectName,
                 "anyRef",
-                "blockedFileExtension"))
+                "blockedFileExtension",
+                ImmutableListMultimap.of()))
         .isFalse();
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/ProjectAwareValidatorConfigTest.java b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/ProjectAwareValidatorConfigTest.java
index c968ec5..6c3a059 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/ProjectAwareValidatorConfigTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/ProjectAwareValidatorConfigTest.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.collect.ImmutableListMultimap;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.IdentifiedUser;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -30,7 +31,9 @@
     ValidatorConfig config =
         getConfig("[plugin \"uploadvalidator\"]\n" + "blockedFileExtension = jar", projectName);
 
-    assertThat(config.isEnabled(anyUser, projectName, "anyRef", "blockedFileExtension"))
+    assertThat(
+            config.isEnabled(
+                anyUser, projectName, "anyRef", "blockedFileExtension", ImmutableListMultimap.of()))
         .isTrue();
   }
 
@@ -43,7 +46,9 @@
                 + "   blockedFileExtension = jar",
             projectName);
 
-    assertThat(config.isEnabled(anyUser, projectName, "anyRef", "blockedFileExtension"))
+    assertThat(
+            config.isEnabled(
+                anyUser, projectName, "anyRef", "blockedFileExtension", ImmutableListMultimap.of()))
         .isTrue();
   }
 
@@ -56,7 +61,9 @@
                 + "   blockedFileExtension = jar",
             projectName);
 
-    assertThat(config.isEnabled(anyUser, projectName, "anyRef", "blockedFileExtension"))
+    assertThat(
+            config.isEnabled(
+                anyUser, projectName, "anyRef", "blockedFileExtension", ImmutableListMultimap.of()))
         .isFalse();
   }
 
@@ -70,9 +77,17 @@
     ValidatorConfig config = getConfig(configString, projectName);
     ValidatorConfig config2 = getConfig(configString, otherNameKey);
 
-    assertThat(config.isEnabled(anyUser, projectName, "anyRef", "blockedFileExtension"))
+    assertThat(
+            config.isEnabled(
+                anyUser, projectName, "anyRef", "blockedFileExtension", ImmutableListMultimap.of()))
         .isTrue();
-    assertThat(config2.isEnabled(anyUser, otherNameKey, "anyRef", "blockedFileExtension"))
+    assertThat(
+            config2.isEnabled(
+                anyUser,
+                otherNameKey,
+                "anyRef",
+                "blockedFileExtension",
+                ImmutableListMultimap.of()))
         .isFalse();
   }
 
@@ -89,17 +104,33 @@
     ValidatorConfig config2 = getConfig(configString, anotherNameKey);
     ValidatorConfig config3 = getConfig(configString, someOtherNameKey);
 
-    assertThat(config.isEnabled(anyUser, projectName, "anyRef", "blockedFileExtension"))
+    assertThat(
+            config.isEnabled(
+                anyUser, projectName, "anyRef", "blockedFileExtension", ImmutableListMultimap.of()))
         .isTrue();
-    assertThat(config2.isEnabled(anyUser, anotherNameKey, "anyRef", "blockedFileExtension"))
+    assertThat(
+            config2.isEnabled(
+                anyUser,
+                anotherNameKey,
+                "anyRef",
+                "blockedFileExtension",
+                ImmutableListMultimap.of()))
         .isTrue();
-    assertThat(config3.isEnabled(anyUser, someOtherNameKey, "anyRef", "blockedFileExtension"))
+    assertThat(
+            config3.isEnabled(
+                anyUser,
+                someOtherNameKey,
+                "anyRef",
+                "blockedFileExtension",
+                ImmutableListMultimap.of()))
         .isFalse();
   }
 
   private ValidatorConfig getConfig(String defaultConfig, Project.NameKey projName)
       throws ConfigInvalidException {
     return new ValidatorConfig(
-        new FakeConfigFactory(projName, defaultConfig), new FakeGroupByNameFinder());
+        "uploadvalidator",
+        new FakeConfigFactory(projName, defaultConfig),
+        new FakeGroupByNameFinder());
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/RefAwareValidatorConfigTest.java b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/RefAwareValidatorConfigTest.java
index f74d346..97ae747 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/RefAwareValidatorConfigTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/RefAwareValidatorConfigTest.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.collect.ImmutableListMultimap;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.IdentifiedUser;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -30,7 +31,9 @@
     ValidatorConfig config =
         getConfig("[plugin \"uploadvalidator\"]\n" + "blockedFileExtension = jar");
 
-    assertThat(config.isEnabled(anyUser, projectName, "anyRef", "blockedFileExtension"))
+    assertThat(
+            config.isEnabled(
+                anyUser, projectName, "anyRef", "blockedFileExtension", ImmutableListMultimap.of()))
         .isTrue();
   }
 
@@ -44,7 +47,11 @@
 
     assertThat(
             config.isEnabled(
-                anyUser, projectName, "refs/heads/anyref", "blockedFileExtension"))
+                anyUser,
+                projectName,
+                "refs/heads/anyref",
+                "blockedFileExtension",
+                ImmutableListMultimap.of()))
         .isTrue();
   }
 
@@ -58,7 +65,11 @@
 
     assertThat(
             config.isEnabled(
-                anyUser, projectName, "refs/heads/anyref", "blockedFileExtension"))
+                anyUser,
+                projectName,
+                "refs/heads/anyref",
+                "blockedFileExtension",
+                ImmutableListMultimap.of()))
         .isFalse();
   }
 
@@ -72,11 +83,19 @@
 
     assertThat(
             config.isEnabled(
-                anyUser, projectName, "refs/heads/anotherref", "blockedFileExtension"))
+                anyUser,
+                projectName,
+                "refs/heads/anotherref",
+                "blockedFileExtension",
+                ImmutableListMultimap.of()))
         .isFalse();
     assertThat(
             config.isEnabled(
-                anyUser, projectName, "refs/heads/mybranch123", "blockedFileExtension"))
+                anyUser,
+                projectName,
+                "refs/heads/mybranch123",
+                "blockedFileExtension",
+                ImmutableListMultimap.of()))
         .isTrue();
   }
 
@@ -91,20 +110,34 @@
 
     assertThat(
             config.isEnabled(
-                anyUser, projectName, "refs/heads/branch1", "blockedFileExtension"))
+                anyUser,
+                projectName,
+                "refs/heads/branch1",
+                "blockedFileExtension",
+                ImmutableListMultimap.of()))
         .isTrue();
     assertThat(
             config.isEnabled(
-                anyUser, projectName, "refs/heads/branch2", "blockedFileExtension"))
+                anyUser,
+                projectName,
+                "refs/heads/branch2",
+                "blockedFileExtension",
+                ImmutableListMultimap.of()))
         .isTrue();
     assertThat(
             config.isEnabled(
-                anyUser, projectName, "refs/heads/branch3", "blockedFileExtension"))
+                anyUser,
+                projectName,
+                "refs/heads/branch3",
+                "blockedFileExtension",
+                ImmutableListMultimap.of()))
         .isFalse();
   }
 
   private ValidatorConfig getConfig(String defaultConfig) throws ConfigInvalidException {
     return new ValidatorConfig(
-        new FakeConfigFactory(projectName, defaultConfig), new FakeGroupByNameFinder());
+        "uploadvalidator",
+        new FakeConfigFactory(projectName, defaultConfig),
+        new FakeGroupByNameFinder());
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/SkipValidationTest.java b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/SkipValidationTest.java
index b46ac86..81a4ddc 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/SkipValidationTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/SkipValidationTest.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.collect.ImmutableListMultimap;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.IdentifiedUser;
@@ -25,13 +26,18 @@
 public class SkipValidationTest {
   private final Project.NameKey projectName = Project.nameKey("testProject");
   private final IdentifiedUser anyUser = new FakeUserProvider().get();
+  private static final String pluginName = "uploadvalidator";
 
   @Test
   public void dontSkipByDefault() throws Exception {
     ValidatorConfig validatorConfig =
-        new ValidatorConfig(new FakeConfigFactory(projectName, ""), new FakeGroupByNameFinder());
+        new ValidatorConfig(
+            pluginName, new FakeConfigFactory(projectName, ""), new FakeGroupByNameFinder());
 
-    assertThat(validatorConfig.isEnabled(anyUser, projectName, "anyRef", "anyOp")).isTrue();
+    assertThat(
+            validatorConfig.isEnabled(
+                anyUser, projectName, "anyRef", "anyOp", ImmutableListMultimap.of()))
+        .isTrue();
   }
 
   @Test
@@ -44,14 +50,15 @@
 
     ValidatorConfig validatorConfig =
         new ValidatorConfig(
-            new FakeConfigFactory(projectName, config), new FakeGroupByNameFinder());
+            pluginName, new FakeConfigFactory(projectName, config), new FakeGroupByNameFinder());
 
     assertThat(
             validatorConfig.isEnabled(
                 new FakeUserProvider("testGroup", "yetAnotherGroup").get(),
                 projectName,
                 "anyRef",
-                "testOp"))
+                "testOp",
+                ImmutableListMultimap.of()))
         .isFalse();
   }
 
@@ -62,6 +69,7 @@
 
     ValidatorConfig validatorConfig =
         new ValidatorConfig(
+            pluginName,
             new FakeConfigFactory(projectName, config),
             new FakeGroupByNameFinder(
                 AccountGroup.nameKey("testGroupName"),
@@ -71,7 +79,11 @@
 
     assertThat(
             validatorConfig.isEnabled(
-                new FakeUserProvider("testGroupId").get(), projectName, "anyRef", "testOp"))
+                new FakeUserProvider("testGroupId").get(),
+                projectName,
+                "anyRef",
+                "testOp",
+                ImmutableListMultimap.of()))
         .isFalse();
   }
 
@@ -85,11 +97,15 @@
 
     ValidatorConfig validatorConfig =
         new ValidatorConfig(
-            new FakeConfigFactory(projectName, config), new FakeGroupByNameFinder());
+            pluginName, new FakeConfigFactory(projectName, config), new FakeGroupByNameFinder());
 
     assertThat(
             validatorConfig.isEnabled(
-                new FakeUserProvider("yetAnotherGroup").get(), projectName, "anyRef", "testOp"))
+                new FakeUserProvider("yetAnotherGroup").get(),
+                projectName,
+                "anyRef",
+                "testOp",
+                ImmutableListMultimap.of()))
         .isTrue();
   }
 
@@ -103,9 +119,11 @@
 
     ValidatorConfig validatorConfig =
         new ValidatorConfig(
-            new FakeConfigFactory(projectName, config), new FakeGroupByNameFinder());
+            pluginName, new FakeConfigFactory(projectName, config), new FakeGroupByNameFinder());
 
-    assertThat(validatorConfig.isEnabled(anyUser, projectName, "anyRef", "anotherOp"))
+    assertThat(
+            validatorConfig.isEnabled(
+                anyUser, projectName, "anyRef", "anotherOp", ImmutableListMultimap.of()))
         .isTrue();
   }
 
@@ -119,11 +137,15 @@
 
     ValidatorConfig validatorConfig =
         new ValidatorConfig(
-            new FakeConfigFactory(projectName, config), new FakeGroupByNameFinder());
+            pluginName, new FakeConfigFactory(projectName, config), new FakeGroupByNameFinder());
 
     assertThat(
             validatorConfig.isEnabled(
-                new FakeUserProvider("testGroup").get(), projectName, "refs/heads/myref", "testOp"))
+                new FakeUserProvider("testGroup").get(),
+                projectName,
+                "refs/heads/myref",
+                "testOp",
+                ImmutableListMultimap.of()))
         .isFalse();
   }
 
@@ -137,11 +159,48 @@
 
     ValidatorConfig validatorConfig =
         new ValidatorConfig(
-            new FakeConfigFactory(projectName, config), new FakeGroupByNameFinder());
+            pluginName, new FakeConfigFactory(projectName, config), new FakeGroupByNameFinder());
 
     assertThat(
             validatorConfig.isEnabled(
-                anyUser, projectName, "refs/heads/anotherRef", "testOp"))
+                anyUser,
+                projectName,
+                "refs/heads/anotherRef",
+                "testOp",
+                ImmutableListMultimap.of()))
         .isTrue();
   }
+
+  @Test
+  public void dontSkipOnPushOptionIfNotEnabled() throws Exception {
+    ValidatorConfig validatorConfig =
+        new ValidatorConfig(
+            pluginName, new FakeConfigFactory(projectName, ""), new FakeGroupByNameFinder());
+
+    assertThat(
+            validatorConfig.isEnabled(
+                anyUser,
+                projectName,
+                "anyRef",
+                "anyOp",
+                ImmutableListMultimap.of("uploadvalidator~skip", "")))
+        .isTrue();
+  }
+
+  @Test
+  public void skipOnPushOptionEnabled() throws Exception {
+    String config = "[plugin \"uploadvalidator\"]\n" + "skipViaPushOption=true";
+    ValidatorConfig validatorConfig =
+        new ValidatorConfig(
+            pluginName, new FakeConfigFactory(projectName, config), new FakeGroupByNameFinder());
+
+    assertThat(
+            validatorConfig.isEnabled(
+                anyUser,
+                projectName,
+                "anyRef",
+                "anyOp",
+                ImmutableListMultimap.of("uploadvalidator~skip", "")))
+        .isFalse();
+  }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/UploadValidatorIT.java b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/UploadValidatorIT.java
index e6eceed..1a02d08 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/UploadValidatorIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/UploadValidatorIT.java
@@ -19,6 +19,7 @@
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
@@ -63,6 +64,7 @@
         Joiner.on("\n")
             .join(
                 "[plugin \"uploadvalidator\"]",
+                "    skipViaPushOption = true",
                 "    group = " + adminGroupUuid(),
                 "    blockedFileExtension = jar",
                 "    blockedFileExtension = .zip",
@@ -175,4 +177,16 @@
         .to("refs/heads/master")
         .assertOkStatus();
   }
+
+  @Test
+  public void testRulesNotEnforcedForSkipPushOption() throws Exception {
+    PushOneCommit push =
+        pushFactory.create(
+            admin.newIdent(),
+            clone,
+            "Subject",
+            ImmutableMap.of("a.txt", "content\nline2\n", "A.TXT", "content"));
+    push.setPushOptions(ImmutableList.of("uploadvalidator~skip"));
+    push.to("refs/heads/master").assertOkStatus();
+  }
 }