Merge changes I4ecdc08d,I9dc12447,I173903d2

* changes:
  Create a scheduled task to automatically deactivate accounts
  Synchronize account inactive flag with LDAP auth
  Change SetInactiveFlag to accept account id instead of IdentifiedUser
This commit is contained in:
Dave Borowitz
2017-10-01 13:31:04 +00:00
committed by Gerrit Code Review
12 changed files with 271 additions and 11 deletions

View File

@@ -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.
+
----
<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

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

View File

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

View File

@@ -49,6 +49,6 @@ public class DeleteActive implements RestModifyView<AccountResource, Input> {
if (self.get() == rsrc.getUser()) {
throw new ResourceConflictException("cannot deactivate own account");
}
return setInactiveFlag.deactivate(rsrc.getUser());
return setInactiveFlag.deactivate(rsrc.getUser().getAccountId());
}
}

View File

@@ -41,6 +41,6 @@ public class PutActive implements RestModifyView<AccountResource, Input> {
@Override
public Response<String> apply(AccountResource rsrc, Input input)
throws ResourceNotFoundException, OrmException, IOException, ConfigInvalidException {
return setInactiveFlag.activate(rsrc.getUser());
return setInactiveFlag.activate(rsrc.getUser().getAccountId());
}
}

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

@@ -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<String> activate(IdentifiedUser user)
public Response<String> 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);

View File

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