Create a scheduled task to automatically deactivate accounts

This job will perform scheduled sweeps of accounts registered in the
Gerrit to deactivate those which are reported as inactive by the auth
backend. This job is by default disabled. Currently, only the LDAP
implementation is covered.

Change-Id: I4ecdc08df477960f0e884d7e31e174ad6d47e6e0
This commit is contained in:
Owen Li 2017-07-14 09:11:20 -04:00 committed by Hugo Arès
parent c24f7246dd
commit e2877bb28d
7 changed files with 196 additions and 0 deletions

View File

@ -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.
+
----
<day of week> <hours>:<minutes>
or
<hours>:<minutes>
<day of week> : Mon, Tue, Wed, Thu, Fri, Sat, Sun
<hours> : 00-23
<minutes> : 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

View File

@ -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));

View File

@ -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<InternalAccountQuery> accountQueryProvider;
private final Realm realm;
private final SetInactiveFlag sif;
@Inject
AccountDeactivator(
Provider<InternalAccountQuery> 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";
}
}

View File

@ -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;
}
}

View File

@ -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<String, Optional<Account.Id>> {
private final ExternalIds externalIds;

View File

@ -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);

View File

@ -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);
}