diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt index 6e1f3941f6..93910d92d4 100644 --- a/Documentation/config-gerrit.txt +++ b/Documentation/config-gerrit.txt @@ -628,6 +628,23 @@ enable registration of new email addresses. + By default, true. +[[auth.autoUpdateAccountActiveStatus]]auth.autoUpdateAccountActiveStatus:: ++ +Whether to allow automatic synchronization of an account's inactive flag upon login. +If set to true, upon login, if the authentication back-end reports the account as active, +the account's inactive flag in the internal Gerrit database will be updated to be active. +If the authentication back-end reports the account as inactive, the account's flag will be +updated to be inactive and the login attempt will be blocked. Users enabling this feature +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]] === Section cache @@ -4538,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/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java index ec756bcd49..046b60a925 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java @@ -22,6 +22,8 @@ import com.google.gerrit.common.data.Permission; import com.google.gerrit.common.errors.NameAlreadyUsedException; import com.google.gerrit.common.errors.NoSuchGroupException; import com.google.gerrit.extensions.client.AccountFieldName; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.server.ReviewDb; @@ -70,6 +72,8 @@ public class AccountManager { private final ExternalIds externalIds; private final ExternalIdsUpdate.Server externalIdsUpdateFactory; private final GroupsUpdate.Factory groupsUpdateFactory; + private final boolean autoUpdateAccountActiveStatus; + private final SetInactiveFlag setInactiveFlag; @Inject AccountManager( @@ -86,7 +90,8 @@ public class AccountManager { Provider accountQueryProvider, ExternalIds externalIds, ExternalIdsUpdate.Server externalIdsUpdateFactory, - GroupsUpdate.Factory groupsUpdateFactory) { + GroupsUpdate.Factory groupsUpdateFactory, + SetInactiveFlag setInactiveFlag) { this.schema = schema; this.sequences = sequences; this.accounts = accounts; @@ -102,6 +107,9 @@ public class AccountManager { this.externalIds = externalIds; this.externalIdsUpdateFactory = externalIdsUpdateFactory; this.groupsUpdateFactory = groupsUpdateFactory; + this.autoUpdateAccountActiveStatus = + cfg.getBoolean("auth", "autoUpdateAccountActiveStatus", false); + this.setInactiveFlag = setInactiveFlag; } /** @return user identified by this external identity string */ @@ -122,8 +130,8 @@ public class AccountManager { * @param who identity of the user, with any details we received about them. * @return the result of authenticating the user. * @throws AccountException the account does not exist, and cannot be created, or exists, but - * cannot be located, or is inactive, or cannot be added to the admin group (only for the - * first account). + * cannot be located, is unable to be activated or deactivated, or is inactive, or cannot be + * added to the admin group (only for the first account). */ public AuthResult authenticate(AuthRequest who) throws AccountException, IOException { who = realm.authenticate(who); @@ -138,6 +146,24 @@ public class AccountManager { // Account exists Account act = byIdCache.get(id.accountId()).getAccount(); + if (autoUpdateAccountActiveStatus && who.authProvidesAccountActiveStatus()) { + if (who.isActive() && !act.isActive()) { + try { + setInactiveFlag.activate(act.getId()); + act = byIdCache.get(id.accountId()).getAccount(); + } catch (ResourceNotFoundException e) { + throw new AccountException("Unable to activate account " + act.getId(), e); + } + } else if (!who.isActive() && act.isActive()) { + try { + setInactiveFlag.deactivate(act.getId()); + act = byIdCache.get(id.accountId()).getAccount(); + } catch (RestApiException e) { + throw new AccountException("Unable to deactivate account " + act.getId(), e); + } + } + } + if (!act.isActive()) { throw new AccountException("Authentication error, account inactive"); } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java index e654b8d737..6647ca4806 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java @@ -63,6 +63,8 @@ public class AuthRequest { private boolean skipAuthentication; private String authPlugin; private String authProvider; + private boolean authProvidesAccountActiveStatus; + private boolean active; public AuthRequest(ExternalId.Key externalId) { this.externalId = externalId; @@ -140,4 +142,20 @@ public class AuthRequest { public void setAuthProvider(String authProvider) { this.authProvider = authProvider; } + + public boolean authProvidesAccountActiveStatus() { + return authProvidesAccountActiveStatus; + } + + public void setAuthProvidesAccountActiveStatus(boolean authProvidesAccountActiveStatus) { + this.authProvidesAccountActiveStatus = authProvidesAccountActiveStatus; + } + + public boolean isActive() { + return active; + } + + public void setActive(Boolean isActive) { + this.active = isActive; + } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java index 0ff5342c35..43669c0669 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java @@ -49,6 +49,6 @@ public class DeleteActive implements RestModifyView { if (self.get() == rsrc.getUser()) { throw new ResourceConflictException("cannot deactivate own account"); } - return setInactiveFlag.deactivate(rsrc.getUser()); + return setInactiveFlag.deactivate(rsrc.getUser().getAccountId()); } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java index 825ef1056d..7ce2ea8a96 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java @@ -41,6 +41,6 @@ public class PutActive implements RestModifyView { @Override public Response apply(AccountResource rsrc, Input input) throws ResourceNotFoundException, OrmException, IOException, ConfigInvalidException { - return setInactiveFlag.activate(rsrc.getUser()); + return setInactiveFlag.activate(rsrc.getUser().getAccountId()); } } 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/account/SetInactiveFlag.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetInactiveFlag.java index 169838733f..6e12c3ef96 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetInactiveFlag.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetInactiveFlag.java @@ -19,7 +19,6 @@ import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.server.IdentifiedUser; import com.google.inject.Inject; import com.google.inject.Singleton; import java.io.IOException; @@ -36,14 +35,14 @@ public class SetInactiveFlag { this.accountsUpdate = accountsUpdate; } - public Response deactivate(IdentifiedUser user) + public Response deactivate(Account.Id accountId) throws RestApiException, IOException, ConfigInvalidException { AtomicBoolean alreadyInactive = new AtomicBoolean(false); Account account = accountsUpdate .create() .update( - user.getAccountId(), + accountId, a -> { if (!a.isActive()) { alreadyInactive.set(true); @@ -60,14 +59,14 @@ public class SetInactiveFlag { return Response.none(); } - public Response activate(IdentifiedUser user) + public Response activate(Account.Id accountId) throws ResourceNotFoundException, IOException, ConfigInvalidException { AtomicBoolean alreadyActive = new AtomicBoolean(false); Account account = accountsUpdate .create() .update( - user.getAccountId(), + accountId, a -> { if (a.isActive()) { alreadyActive.set(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 a34e3fcadb..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 @@ -33,6 +33,7 @@ import com.google.gerrit.server.account.GroupBackends; import com.google.gerrit.server.account.externalids.ExternalId; import com.google.gerrit.server.account.externalids.ExternalIds; import com.google.gerrit.server.auth.AuthenticationUnavailableException; +import com.google.gerrit.server.auth.NoSuchUserException; import com.google.gerrit.server.config.AuthConfig; import com.google.gerrit.server.config.GerritServerConfig; import com.google.inject.Inject; @@ -232,7 +233,15 @@ class LdapRealm extends AbstractRealm { } try { final Helper.LdapSchema schema = helper.getSchema(ctx); - final LdapQuery.Result m = helper.findAccount(schema, ctx, username, fetchMemberOfEagerly); + LdapQuery.Result m; + who.setAuthProvidesAccountActiveStatus(true); + try { + m = helper.findAccount(schema, ctx, username, fetchMemberOfEagerly); + who.setActive(true); + } catch (NoSuchUserException e) { + who.setActive(false); + return who; + } if (authConfig.getAuthType() == AuthType.LDAP && !who.isSkipAuthentication()) { // We found the user account, but we need to verify @@ -314,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); }