diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt index b21501834a..93910d92d4 100644 --- a/Documentation/config-gerrit.txt +++ b/Documentation/config-gerrit.txt @@ -638,6 +638,11 @@ updated to be inactive and the login attempt will be blocked. Users enabling thi should ensure that their authentication back-end is supported. Currently, only strict 'LDAP' authentication is supported. + +In addition, if this parameter is not set, or false, the corresponding scheduled +task to deactivate inactive Gerrit accounts will also be disabled. If this +parameter is set to true, users should also consider configuring the +link:#accountDeactivation[accountDeactivation] section appropriately. ++ By default, false. [[cache]] @@ -4550,6 +4555,44 @@ on the server. One or more groups can be set. If no groups are added, any user will be allowed to execute 'upload-pack' on the server. +[[accountDeactivation]] +=== Section accountDeactivation + +Configures the parameters for the scheduled task to sweep and deactivate Gerrit +accounts according to their status reported by the auth backend. Currently only +supported for LDAP backends. + +[[accountDeactivation.startTime]]accountDeactivation.startTime:: ++ +Start time to define the first execution of account deactivations. +If the configured `'accountDeactivation.interval'` is shorter than `'accountDeactivation.startTime - now'` +the start time will be preponed by the maximum integral multiple of +`'accountDeactivation.interval'` so that the start time is still in the future. ++ +---- + : +or +: + + : Mon, Tue, Wed, Thu, Fri, Sat, Sun + : 00-23 + : 0-59 +---- + +[[accountDeactivation.interval]]accountDeactivation.interval:: ++ +Interval for periodic repetition of triggering account deactivation sweeps. +The interval must be larger than zero. The following suffixes are supported +to define the time unit for the interval: ++ +* `s, sec, second, seconds` +* `m, min, minute, minutes` +* `h, hr, hour, hours` +* `d, day, days` +* `w, week, weeks` (`1 week` is treated as `7 days`) +* `mon, month, months` (`1 month` is treated as `30 days`) +* `y, year, years` (`1 year` is treated as `365 days`) + [[urlAlias]] === Section urlAlias diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java index a71a7fae6d..6b5c1574b8 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java @@ -51,6 +51,7 @@ import com.google.gerrit.pgm.util.RuntimeShutdown; import com.google.gerrit.pgm.util.SiteProgram; import com.google.gerrit.server.LibModuleLoader; import com.google.gerrit.server.StartupChecks; +import com.google.gerrit.server.account.AccountDeactivator; import com.google.gerrit.server.account.InternalAccountDirectory; import com.google.gerrit.server.cache.h2.DefaultCacheFactory; import com.google.gerrit.server.change.ChangeCleanupRunner; @@ -461,6 +462,7 @@ public class Daemon extends SiteProgram { }); modules.add(new GarbageCollectionModule()); if (!slave) { + modules.add(new AccountDeactivator.Module()); modules.add(new ChangeCleanupRunner.Module()); } modules.addAll(LibModuleLoader.loadModules(cfgInjector)); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDeactivator.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDeactivator.java new file mode 100644 index 0000000000..c222756d20 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDeactivator.java @@ -0,0 +1,121 @@ +// 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.google.gerrit.server.account; + +import static com.google.gerrit.server.config.ScheduleConfig.MISSING_CONFIG; + +import com.google.gerrit.extensions.events.LifecycleListener; +import com.google.gerrit.lifecycle.LifecycleModule; +import com.google.gerrit.server.config.GerritServerConfig; +import com.google.gerrit.server.config.ScheduleConfig; +import com.google.gerrit.server.git.WorkQueue; +import com.google.gerrit.server.query.account.AccountPredicates; +import com.google.gerrit.server.query.account.InternalAccountQuery; +import com.google.inject.Inject; +import com.google.inject.Provider; +import java.util.concurrent.TimeUnit; +import org.eclipse.jgit.lib.Config; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Runnable to enable scheduling account deactivations to run periodically */ +public class AccountDeactivator implements Runnable { + private static final Logger log = LoggerFactory.getLogger(AccountDeactivator.class); + + public static class Module extends LifecycleModule { + @Override + protected void configure() { + listener().to(Lifecycle.class); + } + } + + static class Lifecycle implements LifecycleListener { + private final WorkQueue queue; + private final AccountDeactivator deactivator; + private final boolean supportAutomaticAccountActivityUpdate; + private final ScheduleConfig scheduleConfig; + + @Inject + Lifecycle(WorkQueue queue, AccountDeactivator deactivator, @GerritServerConfig Config cfg) { + this.queue = queue; + this.deactivator = deactivator; + scheduleConfig = new ScheduleConfig(cfg, "accountDeactivation"); + supportAutomaticAccountActivityUpdate = + cfg.getBoolean("auth", "autoUpdateAccountActiveStatus", false); + } + + @Override + public void start() { + if (!supportAutomaticAccountActivityUpdate) { + return; + } + long interval = scheduleConfig.getInterval(); + long delay = scheduleConfig.getInitialDelay(); + if (delay == MISSING_CONFIG && interval == MISSING_CONFIG) { + log.info("Ignoring missing accountDeactivator schedule configuration"); + } else if (delay < 0 || interval <= 0) { + log.warn( + String.format( + "Ignoring invalid accountDeactivator schedule configuration: %s", scheduleConfig)); + } else { + queue + .getDefaultQueue() + .scheduleAtFixedRate(deactivator, delay, interval, TimeUnit.MILLISECONDS); + } + } + + @Override + public void stop() { + // handled by WorkQueue.stop() already + } + } + + private final Provider accountQueryProvider; + private final Realm realm; + private final SetInactiveFlag sif; + + @Inject + AccountDeactivator( + Provider accountQueryProvider, SetInactiveFlag sif, Realm realm) { + this.accountQueryProvider = accountQueryProvider; + this.sif = sif; + this.realm = realm; + } + + @Override + public void run() { + log.debug("Running account deactivations"); + try { + int numberOfAccountsDeactivated = 0; + for (AccountState acc : accountQueryProvider.get().query(AccountPredicates.isActive())) { + log.debug("processing account " + acc.getUserName()); + if (acc.getUserName() != null && !realm.isActive(acc.getUserName())) { + sif.deactivate(acc.getAccount().getId()); + log.debug("deactivated accout " + acc.getUserName()); + numberOfAccountsDeactivated++; + } + } + log.info( + "Deactivations complete, {} account(s) were deactivated", numberOfAccountsDeactivated); + } catch (Exception e) { + log.error("Failed to deactivate inactive accounts " + e.getMessage(), e); + } + } + + @Override + public String toString() { + return "account deactivator"; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java index b5e4cba18e..c375dd648f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java @@ -19,6 +19,8 @@ import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.server.IdentifiedUser; import java.io.IOException; import java.util.Set; +import javax.naming.NamingException; +import javax.security.auth.login.LoginException; public interface Realm { /** Can the end-user modify this field of their own account? */ @@ -45,4 +47,15 @@ public interface Realm { * into an email address, and then locate the user by that email address. */ Account.Id lookup(String accountName) throws IOException; + + /** + * @return true if the account is active. + * @throws NamingException + * @throws LoginException + * @throws AccountException + */ + default boolean isActive(@SuppressWarnings("unused") String username) + throws LoginException, NamingException, AccountException { + return true; + } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java index 179944428e..ec803e5142 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java @@ -323,6 +323,19 @@ class LdapRealm extends AbstractRealm { } } + @Override + public boolean isActive(String username) + throws LoginException, NamingException, AccountException { + try { + DirContext ctx = helper.open(); + Helper.LdapSchema schema = helper.getSchema(ctx); + helper.findAccount(schema, ctx, username, false); + } catch (NoSuchUserException e) { + return false; + } + return true; + } + static class UserLoader extends CacheLoader> { private final ExternalIds externalIds; diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java index 901084ed16..0e4e8b4122 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java @@ -81,6 +81,7 @@ import com.google.gerrit.server.PluginUser; import com.google.gerrit.server.Sequences; import com.google.gerrit.server.account.AccountCacheImpl; import com.google.gerrit.server.account.AccountControl; +import com.google.gerrit.server.account.AccountDeactivator; import com.google.gerrit.server.account.AccountManager; import com.google.gerrit.server.account.AccountResolver; import com.google.gerrit.server.account.AccountVisibilityProvider; @@ -279,6 +280,7 @@ public class GerritGlobalModule extends FactoryModule { bind(GcConfig.class); bind(ChangeCleanupConfig.class); + bind(AccountDeactivator.class); bind(ApprovalsUtil.class); diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java index 2d4c1d117a..9a02fcdfee 100644 --- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java +++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java @@ -33,6 +33,7 @@ import com.google.gerrit.metrics.dropwizard.DropWizardMetricMaker; import com.google.gerrit.pgm.util.LogFileCompressor; import com.google.gerrit.server.LibModuleLoader; import com.google.gerrit.server.StartupChecks; +import com.google.gerrit.server.account.AccountDeactivator; import com.google.gerrit.server.account.InternalAccountDirectory; import com.google.gerrit.server.cache.h2.DefaultCacheFactory; import com.google.gerrit.server.change.ChangeCleanupRunner; @@ -365,6 +366,7 @@ public class WebAppInitializer extends GuiceServletContextListener implements Fi }); modules.add(new GarbageCollectionModule()); modules.add(new ChangeCleanupRunner.Module()); + modules.add(new AccountDeactivator.Module()); modules.addAll(LibModuleLoader.loadModules(cfgInjector)); return cfgInjector.createChildInjector(modules); }