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:
@@ -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
|
||||
|
||||
|
@@ -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));
|
||||
|
@@ -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";
|
||||
}
|
||||
}
|
@@ -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");
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
|
Reference in New Issue
Block a user