Allow blocking of usernames using wildcards and regex So far, only usernames matching exactly a configured string could be blocked from being used for a service user. This could be tedious. Now, next to exact matching, a wildcard at the end of the block definition can be used, as well as regex, if the pattern string starts with '^'. Change-Id: Idd668efc1fccf2dce009d1ebaca7079486b7c4ea
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/BlockedNameFilter.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/BlockedNameFilter.java new file mode 100644 index 0000000..c365fb7 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/BlockedNameFilter.java
@@ -0,0 +1,81 @@ +// 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.serviceuser; + +import com.google.gerrit.extensions.annotations.PluginName; +import com.google.gerrit.server.config.PluginConfig; +import com.google.gerrit.server.config.PluginConfigFactory; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; + +@Singleton +public class BlockedNameFilter { + private final PluginConfig cfg; + private final Set<String> blockedExactNames = new HashSet<>(); + private final List<String> blockedNamePrefixes = new ArrayList<>(); + private final List<Pattern> blockedRegexNames = new ArrayList<>(); + + @Inject + public BlockedNameFilter(PluginConfigFactory cfgFactory, @PluginName String pluginName) { + this.cfg = cfgFactory.getFromGerritConfig(pluginName); + parseConfig(); + } + + public boolean isBlocked(String username) { + username = username.toLowerCase(); + return isBlockedByExactName(username) + || isBlockedByWildcard(username) + || isBlockedByRegex(username); + } + + private void parseConfig() { + for (String s : cfg.getStringList("block")) { + if (s.startsWith("^")) { + blockedRegexNames.add(Pattern.compile(s, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE)); + } else if (s.endsWith("*")) { + blockedNamePrefixes.add(s.substring(0, s.length() - 1).toLowerCase()); + } else { + blockedExactNames.add(s.toLowerCase()); + } + } + } + + private boolean isBlockedByExactName(String username) { + return blockedExactNames.contains(username); + } + + private boolean isBlockedByWildcard(String username) { + for (String prefix : blockedNamePrefixes) { + if (username.startsWith(prefix)) { + return true; + } + } + return false; + } + + private boolean isBlockedByRegex(String username) { + for (Pattern p : blockedRegexNames) { + if (p.matcher(username).find()) { + return true; + } + } + return false; + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUser.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUser.java index 50fc6a0..7f8b3de 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUser.java +++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUser.java
@@ -16,9 +16,7 @@ import static com.google.gerrit.server.api.ApiUtil.asRestApiException; -import com.google.common.base.Function; import com.google.common.base.Strings; -import com.google.common.collect.Lists; import com.google.gerrit.entities.Account; import com.google.gerrit.entities.Project; import com.google.gerrit.extensions.annotations.PluginName; @@ -55,7 +53,6 @@ import java.util.Arrays; import java.util.Calendar; import java.util.Date; -import java.util.List; import java.util.Locale; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.lib.Config; @@ -79,7 +76,6 @@ private final PluginConfig cfg; private final CreateAccount createAccount; - private final List<String> blockedNames; private final Provider<CurrentUser> userProvider; private final MetaDataUpdate.User metaDataUpdateFactory; private final Project.NameKey allProjects; @@ -87,6 +83,7 @@ private final DateFormat rfc2822DateFormatter; private final Provider<GetConfig> getConfig; private final AccountLoader.Factory accountLoader; + private final BlockedNameFilter blockedNameFilter; @Inject CreateServiceUser( @@ -98,18 +95,10 @@ MetaDataUpdate.User metaDataUpdateFactory, ProjectCache projectCache, Provider<GetConfig> getConfig, - AccountLoader.Factory accountLoader) { + AccountLoader.Factory accountLoader, + BlockedNameFilter blockedNameFilter) { this.cfg = cfgFactory.getFromGerritConfig(pluginName); this.createAccount = createAccount; - this.blockedNames = - Lists.transform( - Arrays.asList(cfg.getStringList("block")), - new Function<String, String>() { - @Override - public String apply(String blockedName) { - return blockedName.toLowerCase(); - } - }); this.userProvider = userProvider; this.metaDataUpdateFactory = metaDataUpdateFactory; this.storage = projectCache.getAllProjects().getConfig(pluginName + ".db"); @@ -119,6 +108,7 @@ Calendar.getInstance(gerritIdent.getTimeZone(), Locale.US)); this.getConfig = getConfig; this.accountLoader = accountLoader; + this.blockedNameFilter = blockedNameFilter; } @Override @@ -146,7 +136,7 @@ throw new BadRequestException("sshKey invalid."); } - if (blockedNames.contains(username.toLowerCase())) { + if (blockedNameFilter.isBlocked(username)) { throw new BadRequestException( "The username '" + username + "' is not allowed as name for service users."); }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/RegisterServiceUser.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/RegisterServiceUser.java index f01b56f..6fe00fc 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/RegisterServiceUser.java +++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/RegisterServiceUser.java
@@ -21,9 +21,7 @@ import static com.googlesource.gerrit.plugins.serviceuser.CreateServiceUser.KEY_OWNER; import static com.googlesource.gerrit.plugins.serviceuser.CreateServiceUser.USER; -import com.google.common.base.Function; import com.google.common.base.Strings; -import com.google.common.collect.Lists; import com.google.gerrit.entities.Account; import com.google.gerrit.entities.Project; import com.google.gerrit.extensions.annotations.PluginName; @@ -59,10 +57,8 @@ import java.io.IOException; import java.text.DateFormat; import java.text.SimpleDateFormat; -import java.util.Arrays; import java.util.Calendar; import java.util.Date; -import java.util.List; import java.util.Locale; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.lib.Config; @@ -82,7 +78,6 @@ private final AccountResolver accountResolver; private final GroupResolver groupResolver; private final PluginConfig cfg; - private final List<String> blockedNames; private final Provider<CurrentUser> userProvider; private final MetaDataUpdate.User metaDataUpdateFactory; private final Project.NameKey allProjects; @@ -90,6 +85,7 @@ private final DateFormat rfc2822DateFormatter; private final AccountLoader.Factory accountLoader; private final PermissionBackend permissionBackend; + private final BlockedNameFilter blockedNameFilter; @Inject RegisterServiceUser( @@ -102,19 +98,11 @@ MetaDataUpdate.User metaDataUpdateFactory, ProjectCache projectCache, AccountLoader.Factory accountLoader, - PermissionBackend permissionBackend) { + PermissionBackend permissionBackend, + BlockedNameFilter blockedNameFilter) { this.accountResolver = accountResolver; this.groupResolver = groupResolver; this.cfg = cfgFactory.getFromGerritConfig(pluginName); - this.blockedNames = - Lists.transform( - Arrays.asList(cfg.getStringList("block")), - new Function<String, String>() { - @Override - public String apply(String blockedName) { - return blockedName.toLowerCase(); - } - }); this.userProvider = userProvider; this.metaDataUpdateFactory = metaDataUpdateFactory; this.storage = projectCache.getAllProjects().getConfig(pluginName + ".db"); @@ -124,6 +112,7 @@ Calendar.getInstance(gerritIdent.getTimeZone(), Locale.US)); this.accountLoader = accountLoader; this.permissionBackend = permissionBackend; + this.blockedNameFilter = blockedNameFilter; } @Override @@ -155,7 +144,7 @@ return Response.none(); } - if (blockedNames.contains(input.username.toLowerCase())) { + if (blockedNameFilter.isBlocked(input.username)) { throw new BadRequestException( "The username '" + input.username + "' is not allowed as name for service users."); }
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md index f52421e..b665fef 100644 --- a/src/main/resources/Documentation/config.md +++ b/src/main/resources/Documentation/config.md
@@ -12,9 +12,17 @@ <a id="block"> `plugin.@PLUGIN@.block` : A username which is forbidden to be used as name for a service - user. The blocked username is case insensitive. Multiple - usernames can be blocked by specifying multiple + user. The blocked username is case insensitive. The match can + either be exact, have a wildcard ('*') at the end or use regular + expressions, which have to start with '^'. If the regex pattern is not + ending with '$', every username starting with a matching prefix will be + blocked. Multiple usernames can be blocked by specifying multiple `plugin.@PLUGIN@.block` entries. + Examples: + [plugin "serviceuser"] + block = johndoe + block = jane* + block = ^gerrit[0-9]* <a id="group"> `plugin.@PLUGIN@.group`
diff --git a/src/test/java/com/googlesource/gerrit/plugins/serviceuser/BlockedNameFilterTest.java b/src/test/java/com/googlesource/gerrit/plugins/serviceuser/BlockedNameFilterTest.java new file mode 100644 index 0000000..997c275 --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/serviceuser/BlockedNameFilterTest.java
@@ -0,0 +1,89 @@ +// 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.serviceuser; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + +import com.google.gerrit.server.config.PluginConfig; +import com.google.gerrit.server.config.PluginConfigFactory; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class BlockedNameFilterTest { + + private static String[] BLOCKED_NAMES = + new String[] { + "exact", "ex*act", "wild*", "^regex[0-9]+", "^ABC", "^[0-9]+$", "regex[0-9]+", "^⁋+" + }; + + private BlockedNameFilter blockedNameFilter; + + @Mock private PluginConfigFactory configFactory; + + @Mock private PluginConfig config; + + @Before + public void setup() { + when(configFactory.getFromGerritConfig("serviceuser")).thenReturn(config); + when(config.getStringList("block")).thenReturn(BLOCKED_NAMES); + blockedNameFilter = new BlockedNameFilter(configFactory, "serviceuser"); + } + + @Test + public void exactMatchIsBlocked() { + assertThat(blockedNameFilter.isBlocked("exact")).isTrue(); + assertThat(blockedNameFilter.isBlocked("ExAct")).isTrue(); + assertThat(blockedNameFilter.isBlocked("ex*act")).isTrue(); + assertThat(blockedNameFilter.isBlocked("regex[0-9]+")).isTrue(); + assertThat(blockedNameFilter.isBlocked("notexact")).isFalse(); + assertThat(blockedNameFilter.isBlocked("exxact")).isFalse(); + } + + @Test + public void wildcardMatchIsBlocked() { + assertThat(blockedNameFilter.isBlocked("wild")).isTrue(); + assertThat(blockedNameFilter.isBlocked("wildcard")).isTrue(); + assertThat(blockedNameFilter.isBlocked("Wilde")).isTrue(); + assertThat(blockedNameFilter.isBlocked("wil")).isFalse(); + } + + @Test + public void regexMatchIsBlocked() { + assertThat(blockedNameFilter.isBlocked("regex1")).isTrue(); + assertThat(blockedNameFilter.isBlocked("Regex1")).isTrue(); + + // Pattern matching is done at the beginning of the username + assertThat(blockedNameFilter.isBlocked("foo-regex1")).isFalse(); + + // Names with unicode characters can be blocked + assertThat(blockedNameFilter.isBlocked("⁋")).isTrue(); + + // Regex matches only complete name, when ending with '$'. + assertThat(blockedNameFilter.isBlocked("01234")).isTrue(); + assertThat(blockedNameFilter.isBlocked("01234abcd")).isFalse(); + + // Regex matches prefix without trailing '$' + assertThat(blockedNameFilter.isBlocked("regex1suffix")).isTrue(); + + // Uppercase regex matches case-insenstive + assertThat(blockedNameFilter.isBlocked("abc")).isTrue(); + assertThat(blockedNameFilter.isBlocked("ABC")).isTrue(); + } +}