Allows exception to the rules

At times people need to be able to avoid the upload validation
rules to actually manage the repository structure or perform
specific operations.

E.g. A release manager should be able to upload some generated
artifacts and thus override the blockage of upload binary files
that is imposed to all other users.

By defining exception to the rules, it is possible to allow
individual users or groups to skip the rules for projects or branches.

Change-Id: I1e11288cd60690754ff4a38499dfcc4385190125
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 cf6f31a..5f4df3d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/BlockedKeywordValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/BlockedKeywordValidator.java
@@ -59,8 +59,9 @@
 import java.util.regex.Pattern;
 
 public class BlockedKeywordValidator implements CommitValidationListener {
+  private static String KEY_CHECK_BLOCKED_KEYWORD = "blockedKeyword";
   private static String KEY_CHECK_BLOCKED_KEYWORD_PATTERN =
-      "blockedKeywordPattern";
+      KEY_CHECK_BLOCKED_KEYWORD + "Pattern";
 
   public static AbstractModule module() {
     return new AbstractModule() {
@@ -111,8 +112,9 @@
       PluginConfig cfg = cfgFactory
           .getFromProjectConfigWithInheritance(
               receiveEvent.project.getNameKey(), pluginName);
-      if (isActive(cfg) && validatorConfig.isEnabledForRef(
-          receiveEvent.getProjectNameKey(), receiveEvent.getRefName())) {
+      if (isActive(cfg)
+          && validatorConfig.isEnabledForRef(receiveEvent.getProjectNameKey(),
+              receiveEvent.getRefName(), KEY_CHECK_BLOCKED_KEYWORD)) {
         ImmutableMap<String, Pattern> blockedKeywordPatterns =
             patternCache.getAll(Arrays
                 .asList(cfg.getStringList(KEY_CHECK_BLOCKED_KEYWORD_PATTERN)));
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 d137252..026cf7a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ContentTypeValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ContentTypeValidator.java
@@ -114,8 +114,9 @@
       PluginConfig cfg = cfgFactory
           .getFromProjectConfigWithInheritance(
               receiveEvent.project.getNameKey(), pluginName);
-      if (isActive(cfg) && validatorConfig.isEnabledForRef(
-          receiveEvent.getProjectNameKey(), receiveEvent.getRefName())) {
+      if (isActive(cfg)
+          && validatorConfig.isEnabledForRef(receiveEvent.getProjectNameKey(),
+              receiveEvent.getRefName(), KEY_BLOCKED_CONTENT_TYPE)) {
         try (Repository repo =
             repoManager.openRepository(receiveEvent.project.getNameKey())) {
           List<CommitValidationMessage> messages =
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 3a44758..8d9e2d1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/DuplicatePathnameValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/DuplicatePathnameValidator.java
@@ -171,8 +171,9 @@
       PluginConfig cfg = cfgFactory
           .getFromProjectConfigWithInheritance(
               receiveEvent.project.getNameKey(), pluginName);
-      if (isActive(cfg) && validatorConfig.isEnabledForRef(
-          receiveEvent.getProjectNameKey(), receiveEvent.getRefName())) {
+      if (isActive(cfg)
+          && validatorConfig.isEnabledForRef(receiveEvent.getProjectNameKey(),
+              receiveEvent.getRefName(), KEY_REJECT_DUPLICATE_PATHNAMES)) {
         locale = getLocale(cfg);
         try (Repository repo =
             repoManager.openRepository(receiveEvent.project.getNameKey())) {
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 3e61ee6..4c13fdf 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/FileExtensionValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/FileExtensionValidator.java
@@ -95,8 +95,9 @@
       PluginConfig cfg = cfgFactory
           .getFromProjectConfigWithInheritance(
               receiveEvent.project.getNameKey(), pluginName);
-      if (isActive(cfg) && validatorConfig.isEnabledForRef(
-          receiveEvent.getProjectNameKey(), receiveEvent.getRefName())) {
+      if (isActive(cfg)
+          && validatorConfig.isEnabledForRef(receiveEvent.getProjectNameKey(),
+              receiveEvent.getRefName(), KEY_BLOCKED_FILE_EXTENSION)) {
         try (Repository repo =
             repoManager.openRepository(receiveEvent.project.getNameKey())) {
           List<CommitValidationMessage> messages = performValidation(repo,
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 3c466d8..1b0e05b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/FooterValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/FooterValidator.java
@@ -81,8 +81,9 @@
               receiveEvent.project.getNameKey(), pluginName);
       String[] requiredFooters =
           cfg.getStringList(KEY_REQUIRED_FOOTER);
-      if (requiredFooters.length > 0 && validatorConfig.isEnabledForRef(
-          receiveEvent.getProjectNameKey(), receiveEvent.getRefName())) {
+      if (requiredFooters.length > 0
+          && validatorConfig.isEnabledForRef(receiveEvent.getProjectNameKey(),
+              receiveEvent.getRefName(), KEY_REQUIRED_FOOTER)) {
         List<CommitValidationMessage> messages = new LinkedList<>();
         Set<String> footers = FluentIterable.from(receiveEvent.commit.getFooterLines())
             .transform(new Function<FooterLine, String>() {
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 d912f5c..75daaf6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/InvalidFilenameValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/InvalidFilenameValidator.java
@@ -59,7 +59,8 @@
     };
   }
 
-  public static String KEY_INVALID_FILENAME_PATTERN = "invalidFilenamePattern";
+  public static String KEY_INVALID_FILENAME = "invalidFilename";
+  public static String KEY_INVALID_FILENAME_PATTERN = KEY_INVALID_FILENAME + "Pattern";
 
   private final String pluginName;
   private final PluginConfigFactory cfgFactory;
@@ -87,8 +88,9 @@
       PluginConfig cfg =
           cfgFactory.getFromProjectConfigWithInheritance(
               receiveEvent.project.getNameKey(), pluginName);
-      if (isActive(cfg) && validatorConfig.isEnabledForRef(
-          receiveEvent.getProjectNameKey(), receiveEvent.getRefName())) {
+      if (isActive(cfg)
+          && validatorConfig.isEnabledForRef(receiveEvent.getProjectNameKey(),
+              receiveEvent.getRefName(), KEY_INVALID_FILENAME)) {
         try (Repository repo = repoManager.openRepository(
             receiveEvent.project.getNameKey())) {
           List<CommitValidationMessage> messages =
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 8e50232..1c04334 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/InvalidLineEndingValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/InvalidLineEndingValidator.java
@@ -99,8 +99,9 @@
       PluginConfig cfg =
           cfgFactory.getFromProjectConfigWithInheritance(
               receiveEvent.project.getNameKey(), pluginName);
-      if (isActive(cfg) && validatorConfig.isEnabledForRef(
-          receiveEvent.getProjectNameKey(), receiveEvent.getRefName())) {
+      if (isActive(cfg)
+          && validatorConfig.isEnabledForRef(receiveEvent.getProjectNameKey(),
+              receiveEvent.getRefName(), KEY_CHECK_REJECT_WINDOWS_LINE_ENDINGS)) {
         try (Repository repo =
             repoManager.openRepository(receiveEvent.project.getNameKey())) {
           List<CommitValidationMessage> messages =
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 f13d16b..5fb3637 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/MaxPathLengthValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/MaxPathLengthValidator.java
@@ -84,8 +84,9 @@
       PluginConfig cfg =
           cfgFactory.getFromProjectConfigWithInheritance(
               receiveEvent.project.getNameKey(), pluginName);
-      if (isActive(cfg) && validatorConfig.isEnabledForRef(
-          receiveEvent.getProjectNameKey(), receiveEvent.getRefName())) {
+      if (isActive(cfg)
+          && validatorConfig.isEnabledForRef(receiveEvent.getProjectNameKey(),
+              receiveEvent.getRefName(), KEY_MAX_PATH_LENGTH)) {
         int maxPathLength = cfg.getInt(KEY_MAX_PATH_LENGTH, 0);
         try (Repository repo =
             repoManager.openRepository(receiveEvent.project.getNameKey())) {
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 e1a492f..cb651fc 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/SubmoduleValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/SubmoduleValidator.java
@@ -86,8 +86,9 @@
       PluginConfig cfg = cfgFactory
           .getFromProjectConfigWithInheritance(
               receiveEvent.project.getNameKey(), pluginName);
-      if (isActive(cfg) && validatorConfig.isEnabledForRef(
-          receiveEvent.getProjectNameKey(), receiveEvent.getRefName())) {
+      if (isActive(cfg)
+          && validatorConfig.isEnabledForRef(receiveEvent.getProjectNameKey(),
+              receiveEvent.getRefName(), KEY_CHECK_SUBMODULE)) {
         try (Repository repo =
             repoManager.openRepository(receiveEvent.project.getNameKey())) {
           List<CommitValidationMessage> messages =
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 f3c4123..8a0e164 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/SymlinkValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/SymlinkValidator.java
@@ -87,8 +87,9 @@
       PluginConfig cfg =
           cfgFactory.getFromProjectConfigWithInheritance(
               receiveEvent.project.getNameKey(), pluginName);
-      if (isActive(cfg) && validatorConfig.isEnabledForRef(
-          receiveEvent.getProjectNameKey(), receiveEvent.getRefName())) {
+      if (isActive(cfg)
+          && validatorConfig.isEnabledForRef(receiveEvent.getProjectNameKey(),
+              receiveEvent.getRefName(), KEY_CHECK_SYMLINK)) {
         try (Repository repo =
             repoManager.openRepository(receiveEvent.project.getNameKey())) {
           List<CommitValidationMessage> messages =
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 3a48288..5f02a69 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ValidatorConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ValidatorConfig.java
@@ -15,64 +15,118 @@
 package com.googlesource.gerrit.plugins.uploadvalidator;
 
 import com.google.gerrit.common.data.RefConfigSection;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.project.RefPatternMatcher;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.Arrays;
+import java.util.stream.Stream;
+
 public class ValidatorConfig {
   private static final Logger log = LoggerFactory
       .getLogger(ValidatorConfig.class);
-
   private final ConfigFactory configFactory;
+  private final Provider<CurrentUser> userProvider;
+  private final GroupCache groupCache;
 
   @Inject
-  public ValidatorConfig(ConfigFactory configFactory) {
+  public ValidatorConfig(ConfigFactory configFactory,
+      Provider<CurrentUser> userProvider, GroupCache groupCache) {
     this.configFactory = configFactory;
+    this.userProvider = userProvider;
+    this.groupCache = groupCache;
   }
 
-  public boolean isEnabledForRef(Project.NameKey projectName, String refName) {
-    PluginConfig pluginConfig = configFactory.get(projectName);
-    if (pluginConfig == null) {
-      log.error("Failed to check if validation is enabled for project "
-          + projectName.get() + ": Plugin config not found");
-      return false;
-    }
-    if(!isValidConfig(pluginConfig, projectName)) {
-      return false;
-    }
+  public boolean isEnabledForRef(Project.NameKey projectName, String refName,
+      String validatorOp) {
+    PluginConfig conf = configFactory.get(projectName);
 
-    String[] refPatterns = pluginConfig.getStringList("ref");
-    if (refPatterns.length == 0) {
-      return true; // Default behavior: no branch-specific config
-    }
-
-    for (String refPattern : refPatterns) {
-      if (match(refName, refPattern)) {
-        return true;
-      }
-    }
-    return false;
+    return conf != null
+        && isValidConfig(conf, projectName)
+        && (activeForRef(conf, refName))
+        && (!hasCriteria(conf, "skipGroup")
+            || !canSkipValidation(conf, validatorOp)
+            || !canSkipRef(conf, refName)
+            || !canSkipGroup(conf));
   }
 
   private boolean isValidConfig(PluginConfig config, Project.NameKey projectName) {
+    return hasValidConfigRef(config, "ref", projectName)
+        && hasValidConfigRef(config, "skipRef", projectName);
+  }
+
+  private boolean hasValidConfigRef(PluginConfig config, String refKey,
+      Project.NameKey projectName) {
     boolean valid = true;
-    for (String refPattern : config.getStringList("ref")) {
+    for (String refPattern : config.getStringList(refKey)) {
       if (!RefConfigSection.isValid(refPattern)) {
         log.error(
-            "Invalid ref name/pattern/regex '{}' in {} project's plugin config",
-            refPattern, projectName.get());
+            "Invalid {} name/pattern/regex '{}' in {} project's plugin config",
+            refKey, refPattern, projectName.get());
         valid = false;
       }
     }
-
     return valid;
   }
 
-  private static boolean match(String refName, String refPattern) {
-    return RefPatternMatcher.getMatcher(refPattern).match(refName, null);
+  private boolean hasCriteria(PluginConfig config, String criteria) {
+    return config.getStringList(criteria).length > 0;
+  }
+
+  private boolean activeForRef(PluginConfig config, String ref) {
+    return matchCriteria(config, "ref", ref, true);
+  }
+
+  private boolean canSkipValidation(PluginConfig config, String validatorOp) {
+    return matchCriteria(config, "skipValidation", validatorOp, false);
+  }
+
+  private boolean canSkipRef(PluginConfig config, String ref) {
+    return matchCriteria(config, "skipRef", ref, true);
+  }
+
+  private boolean matchCriteria(PluginConfig config, String criteria,
+      String value, boolean allowRegex) {
+    boolean match = true;
+    for (String s : config.getStringList(criteria)) {
+      if ((allowRegex && match(value, s)) || (!allowRegex && s.equals(value))) {
+        return true;
+      }
+      match = false;
+    }
+    return match;
+  }
+
+  private static boolean match(String value, String pattern) {
+    return RefPatternMatcher.getMatcher(pattern).match(value, null);
+  }
+
+  private boolean canSkipGroup(PluginConfig conf) {
+    CurrentUser user = userProvider.get();
+    if (!user.isIdentifiedUser()) {
+      return false;
+    }
+
+    Stream<AccountGroup.UUID> skipGroups =
+        Arrays.stream(conf.getStringList("skipGroup")).map(this::groupUUID);
+    return user.asIdentifiedUser().getEffectiveGroups()
+        .containsAnyOf(skipGroups::iterator);
+  }
+
+  private AccountGroup.UUID groupUUID(String groupNameOrUUID) {
+    AccountGroup group =
+        groupCache.get(new AccountGroup.NameKey(groupNameOrUUID));
+    if (group == null) {
+      return new AccountGroup.UUID(groupNameOrUUID);
+    }
+    return group.getGroupUUID();
   }
 }
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index f958126..398a771 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -1,8 +1,5 @@
-@PLUGIN@
-========
-
-Configuration
--------------
+@PLUGIN@ Configuration
+======================
 
 The configuration of the @PLUGIN@ plugin is done on project level in
 the `project.config` file of the project.
@@ -227,3 +224,88 @@
     ref = refs/heads/master
     ref = ^refs/heads/stable-.*
 ```
+
+Permission to skip the rules
+----------------------------
+
+Some users may be allowed to skip some of the rules on a per project and
+per repository basis by configuring the appropriate "skip" settings in the
+project.config.
+
+Skip of the rules is controlled by:
+
+plugin.@PLUGIN@.skipGroup
+:	Group names or UUIDs allowed to skip the rules.
+
+	Groups that are allowed to skip the rules.
+
+	Multiple values are supported.
+	Default: nobody is allowed to skip the rules (empty).
+
+	NOTE: When skipGroup isn't defined, all the other skip settings are ignored.
+
+plugin.@PLUGIN@.skipRef
+:	Ref name, pattern or regexp of the branch to skip.
+
+	List of specific ref names, ref patterns, or regular expressions
+	of the branches where Groups defined in skipGroup are allowed to
+	skip the rules.
+
+	Multiple values are supported.
+	Default: skip validation on all branches for commits pushed by a member of
+	a group listed in skipGroup
+
+plugin.@PLUGIN@.skipValidation
+:	Specific validation to be skipped.
+
+	List of specific validation operations allowed to be skipped by
+	the Groups defined in skipGroup on the branches defined in skipRef.
+
+	Validations can be one of the following strings:
+
+	- blockedContentType
+	- blockedFileExtension
+	- blockedKeyword
+	- invalidFilename
+	- maxPathLength
+	- rejectDuplicatePathnames
+	- rejectSubmodule
+	- rejectSymlink
+	- rejectWindowsLineEndings
+	- requiredFooter
+
+	Multiple values are supported.
+	Default: groups defined at skipGroup can skip all the validation rules.
+
+NOTE: Skip of the validations are inherited from parent projects. The definition
+of the skip criteria on All-Projects automatically apply to every project.
+
+The simplest configuration is to allow a specific group (e.g. Administrators)
+to skip all the rules:
+
+```
+   [plugin "@PLUGIN@"]
+     skipGroup = Administrators
+```
+
+A typical configuration would be to enable validation for a set of branches,
+while excluding a few of them.
+```
+   [plugin "@PLUGIN@"]
+       ref = ^refs/heads/stable-.*
+       skipGroup = release-manager
+       skipRef = refs/heads/stable-3.4
+       skipRef = refs/heads/stable-5.6
+```
+
+A more complex configuration is to allow a set of groups from LDAP, the ReleaseManager
+and GerritAdmins, to push any content to any file extension but only for the master branch:
+
+```
+  [plugin "@PLUGIN@"]
+    skipValidation = blockedFileExtension
+    skipValidation = blockedContentType
+    skipGroup = ldap/ReleaseManagers
+    skipGroup = ldap/GerritAdmins
+    skipRef = refs/heads/master
+```
\ No newline at end of file
diff --git a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/FakeGroupCacheUUIDByName.java b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/FakeGroupCacheUUIDByName.java
new file mode 100644
index 0000000..8cefe93
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/FakeGroupCacheUUIDByName.java
@@ -0,0 +1,70 @@
+// Copyright (C) 2017 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.collect.ImmutableList;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroup.Id;
+import com.google.gerrit.reviewdb.client.AccountGroup.NameKey;
+import com.google.gerrit.reviewdb.client.AccountGroup.UUID;
+import com.google.gerrit.server.account.GroupCache;
+
+import java.io.IOException;
+
+public class FakeGroupCacheUUIDByName implements GroupCache {
+  private AccountGroup accountGroup;
+
+  public FakeGroupCacheUUIDByName(AccountGroup accountGroup) {
+    this.accountGroup = accountGroup;
+  }
+
+  public FakeGroupCacheUUIDByName() {
+    // TODO Auto-generated constructor stub
+  }
+
+  @Override
+  public AccountGroup get(Id groupId) {
+    return null;
+  }
+
+  @Override
+  public AccountGroup get(NameKey name) {
+    return accountGroup != null && accountGroup.getNameKey().equals(name)
+        ? accountGroup : null;
+  }
+
+  @Override
+  public AccountGroup get(UUID uuid) {
+    return null;
+  }
+
+  @Override
+  public ImmutableList<AccountGroup> all() {
+    return null;
+  }
+
+  @Override
+  public void onCreateGroup(NameKey newGroupName) throws IOException {
+  }
+
+  @Override
+  public void evict(AccountGroup group) throws IOException {
+  }
+
+  @Override
+  public void evictAfterRename(NameKey oldName, NameKey newName)
+      throws IOException {
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/FakeGroupMembership.java b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/FakeGroupMembership.java
new file mode 100644
index 0000000..9edec40
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/FakeGroupMembership.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2017 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.reviewdb.client.AccountGroup.UUID;
+import com.google.gerrit.server.account.GroupMembership;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+public class FakeGroupMembership implements GroupMembership {
+  private final Set<String> memberOfGroup = new HashSet<>();
+
+  public FakeGroupMembership(String... memberOfGroup) {
+    this.memberOfGroup.addAll(Arrays.asList(memberOfGroup));
+  }
+
+  @Override
+  public boolean contains(UUID groupId) {
+    return memberOfGroup.contains(groupId.get());
+  }
+
+  @Override
+  public boolean containsAnyOf(Iterable<UUID> groupIds) {
+    return !intersection(groupIds).isEmpty();
+  }
+
+  @Override
+  public Set<UUID> intersection(Iterable<UUID> groupIds) {
+    return StreamSupport.stream(groupIds.spliterator(), false)
+        .filter(this::contains).collect(Collectors.toSet());
+  }
+
+  @Override
+  public Set<UUID> getKnownGroups() {
+    return new HashSet<>();
+  }
+
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/FakeUserProvider.java b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/FakeUserProvider.java
new file mode 100644
index 0000000..cd4f881
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/FakeUserProvider.java
@@ -0,0 +1,44 @@
+// Copyright (C) 2017 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 org.easymock.EasyMock.*;
+
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.inject.Provider;
+
+public class FakeUserProvider implements Provider<CurrentUser> {
+  private final String[] groupUUID;
+
+  public FakeUserProvider(String... groupUUID) {
+    this.groupUUID = groupUUID;
+  }
+
+  @Override
+  public CurrentUser get() {
+    return createNew();
+  }
+
+  private IdentifiedUser createNew() {
+    IdentifiedUser user = createMock(IdentifiedUser.class);
+    expect(user.isIdentifiedUser()).andReturn(true);
+    expect(user.asIdentifiedUser()).andReturn(user);
+    expect(user.getEffectiveGroups()).andReturn(
+        new FakeGroupMembership(groupUUID));
+    replay(user);
+    return user;
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/FileExtensionValidatorTest.java b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/FileExtensionValidatorTest.java
index b2421f0..0b8af8a 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/FileExtensionValidatorTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/FileExtensionValidatorTest.java
@@ -1,3 +1,17 @@
+// Copyright (C) 2017 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;
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 962963b..42a2217 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/RefAwareValidatorConfigTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/RefAwareValidatorConfigTest.java
@@ -14,9 +14,9 @@
 
 package com.googlesource.gerrit.plugins.uploadvalidator;
 
-import com.google.gerrit.reviewdb.client.Project;
+import static com.google.common.truth.Truth.assertThat;
 
-import static com.google.common.truth.Truth.*;
+import com.google.gerrit.reviewdb.client.Project;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.junit.Test;
@@ -30,7 +30,9 @@
         getConfig("[plugin \"uploadvalidator\"]\n"
             + "blockedFileExtension = jar");
 
-    assertThat(config.isEnabledForRef(projectName, "anyRef")).isTrue();
+    assertThat(
+        config.isEnabledForRef(projectName, "anyRef", "blockedFileExtension"))
+        .isTrue();
   }
 
   @Test
@@ -40,8 +42,9 @@
             + "   ref = refs/heads/anyref\n"
             + "   blockedFileExtension = jar");
 
-    assertThat(config.isEnabledForRef(projectName, "refs/heads/anyref"))
-        .isTrue();
+    assertThat(
+        config.isEnabledForRef(projectName, "refs/heads/anyref",
+            "blockedFileExtension")).isTrue();
   }
 
   @Test
@@ -51,8 +54,9 @@
             + "   ref = anInvalidRef\n"
             + "   blockedFileExtension = jar");
 
-    assertThat(config.isEnabledForRef(projectName, "refs/heads/anyref"))
-        .isFalse();
+    assertThat(
+        config.isEnabledForRef(projectName, "refs/heads/anyref",
+            "blockedFileExtension")).isFalse();
   }
 
   @Test
@@ -62,10 +66,12 @@
             + "   ref = ^refs/heads/mybranch.*\n"
             + "   blockedFileExtension = jar");
 
-    assertThat(config.isEnabledForRef(projectName, "refs/heads/anotherref"))
-        .isFalse();
-    assertThat(config.isEnabledForRef(projectName, "refs/heads/mybranch123"))
-        .isTrue();
+    assertThat(
+        config.isEnabledForRef(projectName, "refs/heads/anotherref",
+            "blockedFileExtension")).isFalse();
+    assertThat(
+        config.isEnabledForRef(projectName, "refs/heads/mybranch123",
+            "blockedFileExtension")).isTrue();
   }
 
   @Test
@@ -76,18 +82,22 @@
             + "   ref = refs/heads/branch2\n"
             + "   blockedFileExtension = jar");
 
-    assertThat(config.isEnabledForRef(projectName, "refs/heads/branch1"))
-        .isTrue();
-    assertThat(config.isEnabledForRef(projectName, "refs/heads/branch2"))
-        .isTrue();
-    assertThat(config.isEnabledForRef(projectName, "refs/heads/branch3"))
-        .isFalse();
+    assertThat(
+        config.isEnabledForRef(projectName, "refs/heads/branch1",
+            "blockedFileExtension")).isTrue();
+    assertThat(
+        config.isEnabledForRef(projectName, "refs/heads/branch2",
+            "blockedFileExtension")).isTrue();
+    assertThat(
+        config.isEnabledForRef(projectName, "refs/heads/branch3",
+            "blockedFileExtension")).isFalse();
   }
 
   private ValidatorConfig getConfig(String defaultConfig)
       throws ConfigInvalidException {
     ValidatorConfig config =
-        new ValidatorConfig(new FakeConfigFactory(projectName, defaultConfig));
+        new ValidatorConfig(new FakeConfigFactory(projectName, defaultConfig),
+            new FakeUserProvider(), new FakeGroupCacheUUIDByName());
     return config;
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/SkipValidationTest.java b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/SkipValidationTest.java
new file mode 100644
index 0000000..3718018
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/SkipValidationTest.java
@@ -0,0 +1,140 @@
+// Copyright (C) 2017 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 com.google.gerrit.reviewdb.client.AccountGroup;
+
+import com.google.gerrit.reviewdb.client.Project;
+
+import org.junit.Test;
+
+public class SkipValidationTest {
+  private Project.NameKey projectName = new Project.NameKey("testProject");
+
+  @Test
+  public void dontSkipByDefault() throws Exception {
+    ValidatorConfig validatorConfig =
+        new ValidatorConfig(new FakeConfigFactory(projectName, ""),
+            new FakeUserProvider(), new FakeGroupCacheUUIDByName());
+
+    assertThat(validatorConfig.isEnabledForRef(projectName, "anyRef", "anyOp"))
+        .isTrue();
+  }
+
+  @Test
+  public void skipWhenUserBelongsToGroupUUID() throws Exception {
+    String config =
+        "[plugin \"uploadvalidator\"]\n"
+            + "skipValidation=testOp\n"
+            + "skipGroup=testGroup\n"
+            + "skipGroup=anotherGroup";
+
+    ValidatorConfig validatorConfig =
+        new ValidatorConfig(new FakeConfigFactory(projectName, config),
+            new FakeUserProvider("testGroup", "yetAnotherGroup"),
+            new FakeGroupCacheUUIDByName());
+
+    assertThat(validatorConfig.isEnabledForRef(projectName, "anyRef", "testOp"))
+        .isFalse();
+  }
+
+  @Test
+  public void skipWhenUserBelongsToGroupName() throws Exception {
+    String config =
+        "[plugin \"uploadvalidator\"]\n"
+            + "skipValidation=testOp\n"
+            + "skipGroup=testGroupName\n";
+
+    ValidatorConfig validatorConfig =
+        new ValidatorConfig(new FakeConfigFactory(projectName, config),
+            new FakeUserProvider("testGroupId"), new FakeGroupCacheUUIDByName(
+                new AccountGroup(new AccountGroup.NameKey("testGroupName"),
+                    new AccountGroup.Id(1),
+                    new AccountGroup.UUID("testGroupId"))));
+
+    assertThat(validatorConfig.isEnabledForRef(projectName, "anyRef", "testOp"))
+        .isFalse();
+  }
+
+  @Test
+  public void dontSkipWhenUserBelongsToOtherGroupsUUID() throws Exception {
+    String config =
+        "[plugin \"uploadvalidator\"]\n"
+            + "skipValidation=testOp\n"
+            + "skipGroup=testGroup\n"
+            + "skipGroup=anotherGroup";
+
+    ValidatorConfig validatorConfig =
+        new ValidatorConfig(new FakeConfigFactory(projectName, config),
+            new FakeUserProvider("yetAnotherGroup"),
+            new FakeGroupCacheUUIDByName());
+
+    assertThat(validatorConfig.isEnabledForRef(projectName, "anyRef", "testOp"))
+        .isTrue();
+  }
+
+  @Test
+  public void dontSkipForOtherOps() throws Exception {
+    String config =
+        "[plugin \"uploadvalidator\"]\n"
+            + "skipValidation=testOp\n"
+            + "skipGroup=testGroup\n"
+            + "skipGroup=anotherGroup";
+
+    ValidatorConfig validatorConfig =
+        new ValidatorConfig(new FakeConfigFactory(projectName, config),
+            new FakeUserProvider("testGroup", "yetAnotherGroup"),
+            new FakeGroupCacheUUIDByName());
+
+    assertThat(
+        validatorConfig.isEnabledForRef(projectName, "anyRef", "anotherOp"))
+        .isTrue();
+  }
+
+  @Test
+  public void skipOnlyOnSpecificRef() throws Exception {
+    String config =
+        "[plugin \"uploadvalidator\"]\n"
+            + "skipValidation=testOp\n"
+            + "skipRef=refs/heads/myref\n"
+            + "skipGroup=testGroup";
+
+    ValidatorConfig validatorConfig =
+        new ValidatorConfig(new FakeConfigFactory(projectName, config),
+            new FakeUserProvider("testGroup"), new FakeGroupCacheUUIDByName());
+
+    assertThat(
+        validatorConfig.isEnabledForRef(projectName, "refs/heads/myref",
+            "testOp")).isFalse();
+  }
+
+  @Test
+  public void dontSkipOnOtherRefs() throws Exception {
+    String config =
+        "[plugin \"uploadvalidator\"]\n"
+            + "skipValidation=testOp\n"
+            + "skipRef=refs/heads/myref\n"
+            + "skipGroup=testGroup";
+
+    ValidatorConfig validatorConfig =
+        new ValidatorConfig(new FakeConfigFactory(projectName, config),
+            new FakeUserProvider("testGroup"), new FakeGroupCacheUUIDByName());
+
+    assertThat(
+        validatorConfig.isEnabledForRef(projectName, "refs/heads/anotherRef",
+            "testOp")).isTrue();
+  }
+}