Add group-specific validations

If validation rules contain information that shouldn't be public,
administrators can currently limit enforcement only based on email and
skipGroup. That may not be sufficient, especially if the same email
domain is used between those two groups.

This CL allows administrators to enforce validation only on users that
belong to one of specificed groups.

Bug: Issue 14359
Change-Id: I2e31a880c70338dcea93a5d44b030f2d9a539149
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 d418038..38d7a4e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ValidatorConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/ValidatorConfig.java
@@ -101,10 +101,11 @@
 
     return conf != null
         && isValidConfig(conf, projectName)
-        && (activeForRef(conf, refName))
+        && activeForRef(conf, refName)
         && (user == null || activeForEmail(conf, user.getAccount().preferredEmail()))
-        && (activeForProject(conf, projectName.get()))
-        && (!isDisabledValidatorOp(conf, validatorOp))
+        && activeForGroup(conf, user)
+        && activeForProject(conf, projectName.get())
+        && !isDisabledValidatorOp(conf, validatorOp)
         && (!hasCriteria(conf, "skipGroup")
             || !canSkipValidation(conf, validatorOp)
             || !canSkipRef(conf, refName)
@@ -153,6 +154,22 @@
     return matchCriteria(config, "email", email, true, false);
   }
 
+  private boolean activeForGroup(PluginConfig config, @Nullable IdentifiedUser user) {
+    if (user == null) {
+      return true;
+    }
+
+    ImmutableList<UUID> groups =
+        Arrays.stream(config.getStringList("group"))
+            .map(this::groupUUID)
+            .collect(toImmutableList());
+    if (groups.isEmpty()) {
+      return true;
+    }
+
+    return user.getEffectiveGroups().containsAnyOf(groups);
+  }
+
   private boolean canSkipValidation(PluginConfig config, String validatorOp) {
     return matchCriteria(config, "skipValidation", validatorOp, false, false);
   }
@@ -188,7 +205,7 @@
   }
 
   private boolean canSkipGroup(PluginConfig conf, @Nullable IdentifiedUser user) {
-    if (user == null || !user.isIdentifiedUser()) {
+    if (user == null) {
       return false;
     }
 
@@ -196,7 +213,7 @@
         Arrays.stream(conf.getStringList("skipGroup"))
             .map(this::groupUUID)
             .collect(toImmutableList());
-    return user.asIdentifiedUser().getEffectiveGroups().containsAnyOf(skipGroups);
+    return user.getEffectiveGroups().containsAnyOf(skipGroups);
   }
 
   private AccountGroup.UUID groupUUID(String groupNameOrUUID) {
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 878b272..56d6296 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -290,12 +290,33 @@
     project = ^platform/.*
 ```
 
+Group-specific validations
+---------------------------
+
+By default, the validation will be enabled for all users. However, it can be
+limited to particular user group by setting `plugin.@PLUGIN@.group`. The group
+may be configured using a specific group name or UUID. Multiple groups may
+be specified.
+
+NOTE: For [system groups](../../../Documentation/access-control.html#system_groups)
+and external groups (e.g.
+[LDAP groups](../../../Documentation/access-control.html#ldap_groups)) the use
+of UUIDs is required. This is because group names are resolved through the
+group index and the group index only contains Gerrit internal groups.
+
+E.g. to limit the validation to all users that are part of group `foo` the
+following could be configured:
+
+```
+  [plugin "@PLUGIN@"]
+    group = foo
+```
+
 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.
+Some users may be allowed to skip some of the rules by configuring the
+appropriate "skip" settings in the project.config.
 
 Skip of the rules is controlled by:
 
@@ -309,6 +330,9 @@
 
 NOTE: When skipGroup isn't defined, all the other skip settings are ignored.
 
+NOTE: If skipGroup is the same as group, all users are able to skip validations
+based on other skip rules.
+
 NOTE: For [system groups](../../../Documentation/access-control.html#system_groups)
 and external groups (e.g.
 [LDAP groups](../../../Documentation/access-control.html#ldap_groups)) the use
diff --git a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/GroupAwareValidatorConfigTest.java b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/GroupAwareValidatorConfigTest.java
new file mode 100644
index 0000000..82ceced
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/GroupAwareValidatorConfigTest.java
@@ -0,0 +1,105 @@
+// 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.util.time.TimeUtil;
+import org.junit.Test;
+
+public class GroupAwareValidatorConfigTest {
+  private Project.NameKey projectName = Project.nameKey("testProject");
+
+  @Test
+  public void isEnabledForNoGroupsByDefault() throws Exception {
+    String config = "[plugin \"uploadvalidator\"]\n" + "blockedFileExtension = jar\n";
+
+    ValidatorConfig validatorConfig =
+        new ValidatorConfig(
+            new FakeConfigFactory(projectName, config), new FakeGroupByNameFinder());
+
+    assertThat(
+            validatorConfig.isEnabled(
+                new FakeUserProvider().get(), projectName, "anyRef", "blockedFileExtension"))
+        .isTrue();
+  }
+
+  @Test
+  public void isEnabledWhenUserBelongsToOneGroup() throws Exception {
+    String config =
+        "[plugin \"uploadvalidator\"]\n"
+            + "blockedFileExtension = jar\n"
+            + "group=fooGroup\n"
+            + "group=barGroup\n";
+
+    ValidatorConfig validatorConfig =
+        new ValidatorConfig(
+            new FakeConfigFactory(projectName, config), new FakeGroupByNameFinder());
+
+    assertThat(
+            validatorConfig.isEnabled(
+                new FakeUserProvider("fooGroup", "bazGroup").get(),
+                projectName,
+                "anyRef",
+                "blockedFileExtension"))
+        .isTrue();
+  }
+
+  @Test
+  public void isEnabledWhenUserInGroupUUID() throws Exception {
+    String config =
+        "[plugin \"uploadvalidator\"]\n" + "blockedFileExtension = jar\n" + "group=testGroupName\n";
+
+    ValidatorConfig validatorConfig =
+        new ValidatorConfig(
+            new FakeConfigFactory(projectName, config),
+            new FakeGroupByNameFinder(
+                AccountGroup.nameKey("testGroupName"),
+                AccountGroup.id(1),
+                AccountGroup.uuid("testGroupId"),
+                TimeUtil.nowTs()));
+
+    assertThat(
+            validatorConfig.isEnabled(
+                new FakeUserProvider("testGroupId").get(),
+                projectName,
+                "anyRef",
+                "blockedFileExtension"))
+        .isTrue();
+  }
+
+  @Test
+  public void isDisabledWhenUserNotInGroup() throws Exception {
+    String config =
+        "[plugin \"uploadvalidator\"]\n"
+            + "blockedFileExtension = jar\n"
+            + "group=fooGroup\n"
+            + "group=barGroup\n";
+
+    ValidatorConfig validatorConfig =
+        new ValidatorConfig(
+            new FakeConfigFactory(projectName, config), new FakeGroupByNameFinder());
+
+    assertThat(
+            validatorConfig.isEnabled(
+                new FakeUserProvider("bazGroup").get(),
+                projectName,
+                "anyRef",
+                "blockedFileExtension"))
+        .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 a558336..e6eceed 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/UploadValidatorIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/UploadValidatorIT.java
@@ -59,11 +59,11 @@
 
   @Before
   public void setup() throws Exception {
-
     pushConfig(
         Joiner.on("\n")
             .join(
                 "[plugin \"uploadvalidator\"]",
+                "    group = " + adminGroupUuid(),
                 "    blockedFileExtension = jar",
                 "    blockedFileExtension = .zip",
                 "    blockedKeywordPattern = secr3t",
@@ -161,4 +161,18 @@
         .to("refs/heads/master")
         .assertErrorStatus("duplicate pathnames");
   }
+
+  @Test
+  public void testRulesNotEnforcedForNonGroupMembers() throws Exception {
+    TestRepository<InMemoryRepository> userClone =
+        GitUtil.cloneProject(project, registerRepoConnection(project, user));
+    pushFactory
+        .create(
+            user.newIdent(),
+            userClone,
+            "Subject",
+            ImmutableMap.of("a.txt", "content\nline2\n", "A.TXT", "content"))
+        .to("refs/heads/master")
+        .assertOkStatus();
+  }
 }