Merge "Merge branch 'stable-3.3'"
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 13ff20c..d7cdff8 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;
@@ -36,12 +34,12 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectLevelConfig;
 import com.google.gerrit.server.restapi.account.CreateAccount;
 import com.google.inject.Inject;
@@ -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;
@@ -80,13 +77,14 @@
   private final PluginConfig cfg;
   private final Provider<ProjectLevelConfig.Bare> configProvider;
   private final CreateAccount createAccount;
-  private final List<String> blockedNames;
   private final Provider<CurrentUser> userProvider;
   private final MetaDataUpdate.User metaDataUpdateFactory;
   private final Project.NameKey allProjects;
   private final DateFormat rfc2822DateFormatter;
   private final Provider<GetConfig> getConfig;
   private final AccountLoader.Factory accountLoader;
+  private final StorageCache storageCache;
+  private final BlockedNameFilter blockedNameFilter;
 
   @Inject
   CreateServiceUser(
@@ -97,29 +95,24 @@
       Provider<CurrentUser> userProvider,
       @GerritPersonIdent PersonIdent gerritIdent,
       MetaDataUpdate.User metaDataUpdateFactory,
-      ProjectCache projectCache,
+      AllProjectsName allProjects,
       Provider<GetConfig> getConfig,
-      AccountLoader.Factory accountLoader) {
+      AccountLoader.Factory accountLoader,
+      StorageCache storageCache,
+      BlockedNameFilter blockedNameFilter) {
     this.cfg = cfgFactory.getFromGerritConfig(pluginName);
     this.configProvider = configProvider;
     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.allProjects = projectCache.getAllProjects().getProject().getNameKey();
+    this.allProjects = allProjects;
     this.rfc2822DateFormatter = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
     this.rfc2822DateFormatter.setCalendar(
         Calendar.getInstance(gerritIdent.getTimeZone(), Locale.US));
     this.getConfig = getConfig;
     this.accountLoader = accountLoader;
+    this.storageCache = storageCache;
+    this.blockedNameFilter = blockedNameFilter;
   }
 
   @Override
@@ -147,7 +140,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.");
     }
@@ -192,6 +185,7 @@
 
       md.setMessage("Create service user '" + username + "'\n");
       update.commit(md);
+      storageCache.invalidate();
     }
     ServiceUserInfo info = new ServiceUserInfo(response);
     AccountLoader al = accountLoader.create(true);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetOwner.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetOwner.java
index 74b97b2..bf7227b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetOwner.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetOwner.java
@@ -18,7 +18,6 @@
 import static com.googlesource.gerrit.plugins.serviceuser.CreateServiceUser.USER;
 
 import com.google.gerrit.entities.GroupDescription;
-import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.Response;
@@ -26,37 +25,30 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectLevelConfig;
 import com.google.gerrit.server.restapi.group.GroupJson;
 import com.google.gerrit.server.restapi.group.GroupsCollection;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import java.io.IOException;
 
 @Singleton
 class GetOwner implements RestReadView<ServiceUserResource> {
   private final GroupsCollection groups;
-  private final String pluginName;
-  private final ProjectCache projectCache;
   private final GroupJson json;
+  private final StorageCache storageCache;
 
   @Inject
-  GetOwner(
-      GroupsCollection groups,
-      @PluginName String pluginName,
-      ProjectCache projectCache,
-      GroupJson json) {
+  GetOwner(GroupsCollection groups, GroupJson json, StorageCache storageCache) {
     this.groups = groups;
-    this.pluginName = pluginName;
-    this.projectCache = projectCache;
     this.json = json;
+    this.storageCache = storageCache;
   }
 
   @Override
   public Response<GroupInfo> apply(ServiceUserResource rsrc)
-      throws RestApiException, PermissionBackendException {
-    ProjectLevelConfig storage = projectCache.getAllProjects().getConfig(pluginName + ".db");
-    String owner = storage.get().getString(USER, rsrc.getUser().getUserName().get(), KEY_OWNER);
+      throws IOException, RestApiException, PermissionBackendException {
+    String owner =
+        storageCache.get().getString(USER, rsrc.getUser().getUserName().get(), KEY_OWNER);
     if (owner != null) {
       GroupDescription.Basic group =
           groups.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(owner)).getGroup();
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetServiceUser.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetServiceUser.java
index c700da8..784c6b0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetServiceUser.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetServiceUser.java
@@ -21,7 +21,6 @@
 import static javax.servlet.http.HttpServletResponse.SC_OK;
 
 import com.google.gerrit.entities.Account;
-import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -30,42 +29,37 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectLevelConfig;
 import com.google.gerrit.server.restapi.account.GetAccount;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import java.io.IOException;
 import org.eclipse.jgit.lib.Config;
 
 @Singleton
 class GetServiceUser implements RestReadView<ServiceUserResource> {
   private final Provider<GetAccount> getAccount;
-  private final String pluginName;
-  private final ProjectCache projectCache;
   private final GetOwner getOwner;
   private final AccountLoader.Factory accountLoader;
+  private final StorageCache storageCache;
 
   @Inject
   GetServiceUser(
       Provider<GetAccount> getAccount,
-      @PluginName String pluginName,
-      ProjectCache projectCache,
       GetOwner getOwner,
-      AccountLoader.Factory accountLoader) {
+      AccountLoader.Factory accountLoader,
+      StorageCache storageCache) {
     this.getAccount = getAccount;
-    this.pluginName = pluginName;
-    this.projectCache = projectCache;
     this.getOwner = getOwner;
     this.accountLoader = accountLoader;
+    this.storageCache = storageCache;
   }
 
   @Override
   public Response<ServiceUserInfo> apply(ServiceUserResource rsrc)
-      throws RestApiException, PermissionBackendException {
-    ProjectLevelConfig storage = projectCache.getAllProjects().getConfig(pluginName + ".db");
+      throws IOException, RestApiException, PermissionBackendException {
     String username = rsrc.getUser().getUserName().get();
-    Config db = storage.get();
+    Config db = storageCache.get();
     if (!db.getSubsections(USER).contains(username)) {
       throw new ResourceNotFoundException(username);
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ListServiceUsers.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ListServiceUsers.java
index 5e77ce2..a40b989 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ListServiceUsers.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ListServiceUsers.java
@@ -18,7 +18,6 @@
 import static com.googlesource.gerrit.plugins.serviceuser.CreateServiceUser.USER;
 
 import com.google.common.collect.Maps;
-import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -30,8 +29,6 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectLevelConfig;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -45,39 +42,35 @@
 @Singleton
 class ListServiceUsers implements RestReadView<ConfigResource> {
   private final Provider<CurrentUser> userProvider;
-  private final String pluginName;
-  private final ProjectCache projectCache;
   private final AccountCache accountCache;
   private final Provider<ServiceUserCollection> serviceUsers;
   private final Provider<GetServiceUser> getServiceUser;
+  private final StorageCache storageCache;
 
   @Inject
   ListServiceUsers(
       Provider<CurrentUser> userProvider,
-      @PluginName String pluginName,
-      ProjectCache projectCache,
       AccountCache accountCache,
       Provider<ServiceUserCollection> serviceUsers,
-      Provider<GetServiceUser> getServiceUser) {
+      Provider<GetServiceUser> getServiceUser,
+      StorageCache storageCache) {
     this.userProvider = userProvider;
-    this.pluginName = pluginName;
-    this.projectCache = projectCache;
     this.accountCache = accountCache;
     this.serviceUsers = serviceUsers;
     this.getServiceUser = getServiceUser;
+    this.storageCache = storageCache;
   }
 
   @Override
   public Response<Map<String, ServiceUserInfo>> apply(ConfigResource rscr)
       throws IOException, RestApiException, PermissionBackendException, ConfigInvalidException {
-    ProjectLevelConfig storage = projectCache.getAllProjects().getConfig(pluginName + ".db");
     CurrentUser user = userProvider.get();
     if (user == null || !user.isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
 
     Map<String, ServiceUserInfo> accounts = Maps.newTreeMap();
-    Config db = storage.get();
+    Config db = storageCache.get();
     for (String username : db.getSubsections(USER)) {
       Optional<AccountState> account = accountCache.getByUsername(username);
       if (account.isPresent()) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/Module.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/Module.java
index 9dc32a4..89510bb 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/Module.java
@@ -74,6 +74,7 @@
           }
         });
     install(new HttpModule());
+    install(StorageCache.module());
   }
 
   @Provides
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutOwner.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutOwner.java
index 120c464..32ee082 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutOwner.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutOwner.java
@@ -63,6 +63,7 @@
   private final GroupJson json;
   private final Provider<CurrentUser> self;
   private final PermissionBackend permissionBackend;
+  private final StorageCache storageCache;
 
   @Inject
   PutOwner(
@@ -73,7 +74,8 @@
       MetaDataUpdate.User metaDataUpdateFactory,
       GroupJson json,
       Provider<CurrentUser> self,
-      PermissionBackend permissionBackend) {
+      PermissionBackend permissionBackend,
+      StorageCache storageCache) {
     this.getConfig = getConfig;
     this.groups = groups;
     this.configProvider = configProvider;
@@ -82,6 +84,7 @@
     this.json = json;
     this.self = self;
     this.permissionBackend = permissionBackend;
+    this.storageCache = storageCache;
   }
 
   @Override
@@ -122,6 +125,7 @@
       }
       md.setMessage("Set owner for service user '" + rsrc.getUser().getUserName() + "'\n");
       update.commit(md);
+      storageCache.invalidate();
     } catch (ConfigInvalidException e) {
       throw asRestApiException("Invalid configuration", e);
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/RefUpdateListener.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/RefUpdateListener.java
index fef0bc7..421b326 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/RefUpdateListener.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/RefUpdateListener.java
@@ -27,7 +27,6 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.concurrent.Future;
-
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.slf4j.Logger;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/RegisterServiceUser.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/RegisterServiceUser.java
new file mode 100644
index 0000000..17c8ab3
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/RegisterServiceUser.java
@@ -0,0 +1,193 @@
+// 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.gerrit.server.permissions.GlobalPermission.ADMINISTRATE_SERVER;
+import static com.googlesource.gerrit.plugins.serviceuser.CreateServiceUser.KEY_CREATED_AT;
+import static com.googlesource.gerrit.plugins.serviceuser.CreateServiceUser.KEY_CREATED_BY;
+import static com.googlesource.gerrit.plugins.serviceuser.CreateServiceUser.KEY_CREATOR_ID;
+import static com.googlesource.gerrit.plugins.serviceuser.CreateServiceUser.KEY_OWNER;
+import static com.googlesource.gerrit.plugins.serviceuser.CreateServiceUser.USER;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestCollectionCreateView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.group.GroupResolver;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ProjectLevelConfig;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.serviceuser.GetServiceUser.ServiceUserInfo;
+import com.googlesource.gerrit.plugins.serviceuser.RegisterServiceUser.Input;
+import java.io.IOException;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.PersonIdent;
+
+@RequiresCapability(CreateServiceUserCapability.ID)
+@Singleton
+class RegisterServiceUser
+    implements RestCollectionCreateView<ConfigResource, ServiceUserResource, Input> {
+
+  static class Input {
+    String username;
+    String creator;
+    String owner;
+  }
+
+  private final Provider<ProjectLevelConfig.Bare> configProvider;
+  private final AccountResolver accountResolver;
+  private final GroupResolver groupResolver;
+  private final Provider<CurrentUser> userProvider;
+  private final MetaDataUpdate.User metaDataUpdateFactory;
+  private final Project.NameKey allProjects;
+  private final DateFormat rfc2822DateFormatter;
+  private final AccountLoader.Factory accountLoader;
+  private final StorageCache storageCache;
+  private final PermissionBackend permissionBackend;
+  private final BlockedNameFilter blockedNameFilter;
+
+  @Inject
+  RegisterServiceUser(
+      Provider<ProjectLevelConfig.Bare> configProvider,
+      AccountResolver accountResolver,
+      GroupResolver groupResolver,
+      Provider<CurrentUser> userProvider,
+      @GerritPersonIdent PersonIdent gerritIdent,
+      MetaDataUpdate.User metaDataUpdateFactory,
+      AllProjectsName allProjects,
+      AccountLoader.Factory accountLoader,
+      StorageCache storageCache,
+      PermissionBackend permissionBackend,
+      BlockedNameFilter blockedNameFilter) {
+    this.configProvider = configProvider;
+    this.accountResolver = accountResolver;
+    this.groupResolver = groupResolver;
+    this.userProvider = userProvider;
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+    this.allProjects = allProjects;
+    this.rfc2822DateFormatter = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
+    this.rfc2822DateFormatter.setCalendar(
+        Calendar.getInstance(gerritIdent.getTimeZone(), Locale.US));
+    this.accountLoader = accountLoader;
+    this.storageCache = storageCache;
+    this.permissionBackend = permissionBackend;
+    this.blockedNameFilter = blockedNameFilter;
+  }
+
+  @Override
+  public Response<ServiceUserInfo> apply(ConfigResource parentResource, IdString id, Input input)
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
+    CurrentUser requestingUser = userProvider.get();
+    if (requestingUser == null || !requestingUser.isIdentifiedUser()) {
+      throw new AuthException("authentication required");
+    }
+
+    if (input == null) {
+      input = new Input();
+    }
+
+    IdentifiedUser user;
+    try {
+      user = accountResolver.resolve(input.username).asUniqueUser();
+    } catch (UnresolvableAccountException e) {
+      throw new BadRequestException("Username does not exist");
+    }
+
+    if (!requestingUser.getAccountId().equals(user.getAccountId())
+        && !permissionBackend.user(requestingUser).testOrFalse(ADMINISTRATE_SERVER)) {
+      throw new MethodNotAllowedException("Forbidden");
+    }
+
+    if (blockedNameFilter.isBlocked(input.username)) {
+      throw new BadRequestException(
+          "The username '" + input.username + "' is not allowed as name for service users.");
+    }
+
+    String creator;
+    Account.Id creatorId;
+    if (Strings.isNullOrEmpty(input.creator)) {
+      creator = requestingUser.getUserName().orElse(null);
+      creatorId = requestingUser.asIdentifiedUser().getAccountId();
+    } else {
+      creator = input.creator;
+      creatorId = accountResolver.resolve(input.creator).asUniqueUser().getAccountId();
+    }
+    String creationDate = rfc2822DateFormatter.format(new Date());
+
+    String owner = null;
+    if (!Strings.isNullOrEmpty(input.owner)) {
+      try {
+        owner = groupResolver.parse(input.owner).getGroupUUID().toString();
+      } catch (UnresolvableAccountException e) {
+        throw new BadRequestException("The group '" + input.owner + "' does not exist");
+      }
+    }
+
+    try (MetaDataUpdate md = metaDataUpdateFactory.create(allProjects)) {
+      ProjectLevelConfig.Bare update = configProvider.get();
+      update.load(md);
+
+      Config db = update.getConfig();
+      if (db.getSubsections(USER).contains(input.username)) {
+        return Response.none();
+      }
+      db.setInt(USER, input.username, KEY_CREATOR_ID, creatorId.get());
+      if (creator != null) {
+        db.setString(USER, input.username, KEY_CREATED_BY, creator);
+      }
+      if (owner != null) {
+        db.setString(USER, input.username, KEY_OWNER, owner);
+      }
+      db.setString(USER, input.username, KEY_CREATED_AT, creationDate);
+
+      md.setMessage("Create service user '" + input.username + "'\n");
+      update.commit(md);
+      storageCache.invalidate();
+    }
+
+    ServiceUserInfo info = new ServiceUserInfo(new AccountInfo(user.getAccountId().get()));
+    AccountLoader al = accountLoader.create(true);
+    info.createdBy = al.get(creatorId);
+    al.fill();
+    info.createdAt = creationDate;
+    return Response.created(info);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/RegisterServiceUserCommand.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/RegisterServiceUserCommand.java
new file mode 100644
index 0000000..fb8503c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/RegisterServiceUserCommand.java
@@ -0,0 +1,67 @@
+// 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.RequiresCapability;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+@RequiresCapability(CreateServiceUserCapability.ID)
+@CommandMetaData(name = "register", description = "Register Service User")
+class RegisterServiceUserCommand extends SshCommand {
+
+  @Argument(index = 0, required = true, metaVar = "USERNAME", usage = "name of the service user")
+  private String username;
+
+  @Option(
+      name = "--creator",
+      required = false,
+      metaVar = "CREATOR",
+      usage = "name of the creator of the service user")
+  private String creator;
+
+  @Option(
+      name = "--owner",
+      required = false,
+      metaVar = "OWNER",
+      usage = "group that owns the service user")
+  private String owner;
+
+  @Inject private RegisterServiceUser registerServiceUser;
+
+  @Override
+  protected void run()
+      throws IOException, UnloggedFailure, ConfigInvalidException, PermissionBackendException {
+    RegisterServiceUser.Input input = new RegisterServiceUser.Input();
+    input.username = username;
+    input.creator = creator;
+    input.owner = owner;
+
+    try {
+      registerServiceUser.apply(new ConfigResource(), IdString.fromDecoded(username), input);
+    } catch (RestApiException e) {
+      throw die(e.getMessage());
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ServiceUserCollection.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ServiceUserCollection.java
index c5e5d9a..f6f50ac 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ServiceUserCollection.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ServiceUserCollection.java
@@ -21,12 +21,12 @@
 
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.GroupDescription;
-import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ChildCollection;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.server.CurrentUser;
@@ -34,8 +34,6 @@
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectLevelConfig;
 import com.google.gerrit.server.restapi.account.AccountsCollection;
 import com.google.gerrit.server.restapi.group.GroupsCollection;
 import com.google.inject.Inject;
@@ -43,6 +41,7 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
 
 @Singleton
 class ServiceUserCollection implements ChildCollection<ConfigResource, ServiceUserResource> {
@@ -50,40 +49,36 @@
   private final DynamicMap<RestView<ServiceUserResource>> views;
   private final Provider<ListServiceUsers> list;
   private final Provider<AccountsCollection> accounts;
-  private final String pluginName;
-  private final ProjectCache projectCache;
   private final Provider<CurrentUser> userProvider;
   private final GroupsCollection groups;
   private final PermissionBackend permissionBackend;
+  private final StorageCache storageCache;
 
   @Inject
   ServiceUserCollection(
       DynamicMap<RestView<ServiceUserResource>> views,
       Provider<ListServiceUsers> list,
       Provider<AccountsCollection> accounts,
-      @PluginName String pluginName,
-      ProjectCache projectCache,
       Provider<CurrentUser> userProvider,
       GroupsCollection groups,
-      PermissionBackend permissionBackend) {
+      PermissionBackend permissionBackend,
+      StorageCache storageCache) {
     this.views = views;
     this.list = list;
     this.accounts = accounts;
-    this.pluginName = pluginName;
-    this.projectCache = projectCache;
     this.userProvider = userProvider;
     this.groups = groups;
     this.permissionBackend = permissionBackend;
+    this.storageCache = storageCache;
   }
 
   @Override
   public ServiceUserResource parse(ConfigResource parent, IdString id)
       throws ResourceNotFoundException, AuthException, IOException, PermissionBackendException,
-          ConfigInvalidException {
-    ProjectLevelConfig storage = projectCache.getAllProjects().getConfig(pluginName + ".db");
+          ConfigInvalidException, RestApiException {
     IdentifiedUser serviceUser = accounts.get().parse(TopLevelResource.INSTANCE, id).getUser();
-    if (serviceUser == null
-        || !storage.get().getSubsections(USER).contains(serviceUser.getUserName().get())) {
+    Config db = storageCache.get();
+    if (serviceUser == null || !db.getSubsections(USER).contains(serviceUser.getUserName().get())) {
       throw new ResourceNotFoundException(id);
     }
     CurrentUser user = userProvider.get();
@@ -92,7 +87,7 @@
     }
     if (!permissionBackend.user(user).testOrFalse(ADMINISTRATE_SERVER)) {
       String username = serviceUser.getUserName().get();
-      String owner = storage.get().getString(USER, username, KEY_OWNER);
+      String owner = db.getString(USER, username, KEY_OWNER);
       if (owner != null) {
         GroupDescription.Basic group =
             groups.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(owner)).getGroup();
@@ -101,7 +96,7 @@
         }
       } else if (!((IdentifiedUser) user)
           .getAccountId()
-          .equals(Account.id(storage.get().getInt(USER, username, KEY_CREATOR_ID, -1)))) {
+          .equals(Account.id(db.getInt(USER, username, KEY_CREATOR_ID, -1)))) {
         throw new ResourceNotFoundException(id);
       }
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/SshModule.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/SshModule.java
index f775e4c..40a1509 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/SshModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/SshModule.java
@@ -21,5 +21,6 @@
   @Override
   protected void configureCommands() {
     command(CreateServiceUserCommand.class);
+    command(RegisterServiceUserCommand.class);
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/StorageCache.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/StorageCache.java
new file mode 100644
index 0000000..0378347
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/StorageCache.java
@@ -0,0 +1,93 @@
+// 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.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.project.ProjectLevelConfig;
+import com.google.gerrit.server.project.ProjectLevelConfig.Bare;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+import java.util.concurrent.ExecutionException;
+import org.eclipse.jgit.lib.Config;
+
+@Singleton
+public class StorageCache {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private static final String CACHE_NAME = "storage";
+  private static final Object ALL = new Object();
+
+  static CacheModule module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        cache(CACHE_NAME, Object.class, Config.class).loader(Loader.class);
+        bind(StorageCache.class);
+      }
+    };
+  }
+
+  private final LoadingCache<Object, Config> cache;
+
+  @Inject
+  StorageCache(@Named(CACHE_NAME) LoadingCache<Object, Config> cache) {
+    this.cache = cache;
+  }
+
+  public Config get() {
+    try {
+      return cache.get(ALL);
+    } catch (ExecutionException e) {
+      logger.atSevere().withCause(e).log("Cannot load service users");
+      return new Config();
+    }
+  }
+
+  public void invalidate() {
+    cache.invalidate(ALL);
+  }
+
+  static class Loader extends CacheLoader<Object, Config> {
+    private final Provider<Bare> configProvider;
+    private final MetaDataUpdate.Server metaDataUpdateFactory;
+    private final AllProjectsName allProjects;
+
+    @Inject
+    Loader(
+        Provider<ProjectLevelConfig.Bare> configProvider,
+        MetaDataUpdate.Server metaDataUpdateFactory,
+        AllProjectsName allProjects) {
+      this.configProvider = configProvider;
+      this.metaDataUpdateFactory = metaDataUpdateFactory;
+      this.allProjects = allProjects;
+    }
+
+    @Override
+    public Config load(Object key) throws Exception {
+      ProjectLevelConfig.Bare storage = configProvider.get();
+      try (MetaDataUpdate md = metaDataUpdateFactory.create(allProjects)) {
+        storage.load(md);
+      }
+      return storage.getConfig();
+    }
+  }
+}
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index 17a4e29..e19d856 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -36,11 +36,11 @@
     createdAt = Wed, 13 Nov 2013 14:45:00 +0100
 ```
 
-<a id="createdBy">
+<a id="createdBy"></a>
 `user.<service-user-name>.createdBy`
 : The username of the user who created the service user.
 
-<a id="createdAt">
+<a id="createdAt"></a>
 `user.<service-user-name>.createdAt`
 : The date when the service user was created.
 
diff --git a/src/main/resources/Documentation/cmd-register.md b/src/main/resources/Documentation/cmd-register.md
new file mode 100644
index 0000000..3378a2c
--- /dev/null
+++ b/src/main/resources/Documentation/cmd-register.md
@@ -0,0 +1,50 @@
+@PLUGIN@ register
+=================
+
+NAME
+----
+@PLUGIN@ register - Registers an existing user as a service user
+
+SYNOPSIS
+--------
+```
+ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ register
+  --creator <CREATOR>
+  --owner <OWNER>
+  <USERNAME>
+```
+
+DESCRIPTION
+-----------
+Registers an existing user as a service user.
+
+ACCESS
+------
+Caller must be a member of a group that is granted the
+'Create Service User' capability (provided by this plugin) or the
+'Administrate Server' capability. If not possessing the 'Administrate
+Server' capability, the user to be registered as a service user must
+also be the caller.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+OPTIONS
+-------
+
+`--creator`
+:   Username of the user that will be set as the creator of the
+    serviceuser. Defaults to the caller.
+
+`--owner`
+:   ID or name of the group that will own the service user. Defaults
+    to no owner group being set.
+
+EXAMPLES
+--------
+Register a service user:
+
+```
+  $ ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ register --creator admin --owner Administrators username
+```
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index f52421e..aa2e4b5 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -9,30 +9,41 @@
     group = Service Users
 ```
 
-<a id="block">
+<a id="block"></a>
 `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:
 
-<a id="group">
+```
+   [plugin "serviceuser"]
+        block = johndoe
+        block = jane*
+        block = ^gerrit[0-9]*
+```
+
+<a id="group"></a>
 `plugin.@PLUGIN@.group`
 :	The name of an internal group to which newly created service users
 	should be automatically added. Multiple groups can be specified by
 	having multiple `plugin.@PLUGIN@.group` entries.
 
-<a id="infoMessage">
+<a id="infoMessage"></a>
 `plugin.@PLUGIN@.infoMessage`
 :	HTML formatted message that should be displayed on the service user
 	creation screen.
 
-<a id="onSuccessMessage">
+<a id="onSuccessMessage"></a>
 `plugin.@PLUGIN@.onSuccessMessage`
 :	Message that should be displayed after a service user was
 	successfully created.
 
-<a id="allowEmail">
+<a id="allowEmail"></a>
 `plugin.@PLUGIN@.allowEmail`
 :	Whether it is allowed for service user owners to set email
 	addresses for their service users. Independent of this setting
@@ -40,7 +51,7 @@
 	any service user.
 	By default false.
 
-<a id="allowHttpPassword">
+<a id="allowHttpPassword"></a>
 `plugin.@PLUGIN@.allowHttpPassword`
 :	Whether it is allowed for service user owners to generate HTTP
     passwords for their service users. Independent of this setting
@@ -48,12 +59,12 @@
     passwords for any service user.
     By default false.
 
-<a id="allowOwner">
+<a id="allowOwner"></a>
 `plugin.@PLUGIN@.allowOwner`
 :	Whether it is allowed to set an owner group for a service user.
 	By default false.
 
-<a id="createNotes">
+<a id="createNotes"></a>
 `plugin.@PLUGIN@.createNotes`
 :	Whether commits of a service user should be annotated by a Git note
 	that contains information about the current owners of the service
@@ -62,7 +73,7 @@
 	user the 'Forge Committer' access right must be blocked for service
 	users. By default true.
 
-<a id="createNotes">
+<a id="createNotes"></a>
 `plugin.@PLUGIN@.createNotesAsync`
 :	Whether the Git notes on commits that are pushed by a service user
 	should be created asynchronously. By default false.
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();
+  }
+}