Merge "Allows exception to the rules"
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();
+  }
+}