Introduce `ignoreAccountId` setting

Allow the Gerrit admin to specify a list of accounts that should not be
automatically disabled.

This prevents locking important Gerrit system accounts like the main
administrator.

Bug: Issue 338071091
Change-Id: I92dad4a4302c1ac762e74510c59c93f3aab92059
diff --git a/admin/track-and-disable-inactive-users-1.1.groovy b/admin/track-and-disable-inactive-users-1.2.groovy
similarity index 84%
rename from admin/track-and-disable-inactive-users-1.1.groovy
rename to admin/track-and-disable-inactive-users-1.2.groovy
index 2131aa4..68ebb43 100644
--- a/admin/track-and-disable-inactive-users-1.1.groovy
+++ b/admin/track-and-disable-inactive-users-1.2.groovy
@@ -23,12 +23,14 @@
 import com.google.gerrit.server.*
 import com.google.gerrit.server.account.*
 import com.google.gerrit.server.cache.*
+import com.google.gerrit.server.config.*
 import com.google.gerrit.server.project.*
 import com.google.inject.*
 import com.google.inject.name.*
 
 import java.time.*
 import java.util.function.*
+import java.util.stream.Collectors
 
 import static java.util.concurrent.TimeUnit.*
 
@@ -84,6 +86,30 @@
     return false
   }
 }
+class AutoDisableInactiveUsersConfig {
+  final Set<Account.Id> ignoreAccountIds
+
+  private final PluginConfig config
+
+  @Inject
+  AutoDisableInactiveUsersConfig(
+      PluginConfigFactory configFactory,
+      @PluginName String pluginName
+  ) {
+    config = configFactory.getFromGerritConfig(pluginName)
+
+    ignoreAccountIds = ignoreAccountIdsFromConfig("ignoreAccountId")
+  }
+
+  private Set<Account.Id> ignoreAccountIdsFromConfig(String name) {
+    def strings = config.getStringList(name) as Set
+    strings.stream()
+        .map(Account.Id.&tryParse)
+        .filter { it.isPresent() }
+        .map { it.get() }
+        .collect(Collectors.toSet())
+  }
+}
 
 class AutoDisableInactiveUsersEvictionListener implements CacheRemovalListener<Integer, Long> {
   static final FluentLogger logger = FluentLogger.forEnclosingClass()
@@ -92,15 +118,18 @@
   private final String fullCacheName
   private final Cache<Integer, Long> trackActiveUsersCache
   private final Provider<AccountsUpdate> accountsUpdate
+  private final AutoDisableInactiveUsersConfig autoDisableConfig
 
   @Inject
   AutoDisableInactiveUsersEvictionListener(
       @PluginName String pluginName,
       @ServerInitiated Provider<AccountsUpdate> accountsUpdate,
-      @Named(TrackActiveUsersCache.NAME) Cache<Integer, Long> trackActiveUsersCache
+      @Named(TrackActiveUsersCache.NAME) Cache<Integer, Long> trackActiveUsersCache,
+      AutoDisableInactiveUsersConfig autoDisableConfig
   ) {
     this.pluginName = pluginName
     this.accountsUpdate = accountsUpdate
+    this.autoDisableConfig = autoDisableConfig
     this.trackActiveUsersCache = trackActiveUsersCache
     fullCacheName = "${pluginName}.${TrackActiveUsersCache.NAME}"
   }
@@ -121,8 +150,11 @@
   }
 
   private void disableAccount(Account.Id accountId) {
-    logger.atInfo().log("Automatically disabling user id: %d", accountId.get())
+    if (autoDisableConfig.ignoreAccountIds.contains(accountId)) {
+      return
+    }
 
+    logger.atInfo().log("Automatically disabling user id: %d", accountId.get())
     accountsUpdate.get().update(
         """Automatically disabling after inactivity
 
diff --git a/admin/track-and-disable-inactive-users.md b/admin/track-and-disable-inactive-users.md
index 6a91de5..c569d0b 100644
--- a/admin/track-and-disable-inactive-users.md
+++ b/admin/track-and-disable-inactive-users.md
@@ -20,6 +20,16 @@
 Configuration parameters
 ---------------------
 
+=======
+```plugin.@PLUGIN@.ignoreAccountId```
+:  Specify an account Id that should not be auto disabled.
+   May be specified more than once to specify multiple account Ids, for example:
+
+   ```
+   ignoreAccountId = 1000001
+   ignoreAccountId = 1000002
+   ```
+
 ```cache."@PLUGIN@.users_cache".maxAge```
 :  Maximum allowed inactivity time for user.
    Value should use common time unit suffixes to express their setting: