Add ability to deactivate a user when they leave the project.

Add a inactive column to the Account object.  Use the inactive
status to disable the user's web and ssh logins, sending
emails to the user on behalf of gerrit, adding the user as a
reviewer or to a group, and making the user appear in the
"add reviewer" and group "add member" auto completion boxes.

Bug: issue 503
Change-Id: Ib002788ebf8204dfea608d9f5ac3a5cdff20f817
This commit is contained in:
Martin Fick 2010-08-09 17:41:31 -06:00
parent b7ffd33fbf
commit 3f8385ba1e
20 changed files with 127 additions and 14 deletions

View File

@ -50,6 +50,9 @@ public class ReviewerResult {
/** Name supplied does not match to a registered account. */ /** Name supplied does not match to a registered account. */
ACCOUNT_NOT_FOUND, ACCOUNT_NOT_FOUND,
/** The account is inactive. */
ACCOUNT_INACTIVE,
/** The account is not permitted to see the change. */ /** The account is not permitted to see the change. */
CHANGE_NOT_VISIBLE, CHANGE_NOT_VISIBLE,

View File

@ -28,7 +28,7 @@ public interface SuggestService extends RemoteJsonService {
void suggestProjectNameKey(String query, int limit, void suggestProjectNameKey(String query, int limit,
AsyncCallback<List<Project.NameKey>> callback); AsyncCallback<List<Project.NameKey>> callback);
void suggestAccount(String query, int limit, void suggestAccount(String query, Boolean enabled, int limit,
AsyncCallback<List<AccountInfo>> callback); AsyncCallback<List<AccountInfo>> callback);
void suggestAccountGroup(String query, int limit, void suggestAccountGroup(String query, int limit,

View File

@ -0,0 +1,26 @@
// Copyright (C) 2010 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.common.errors;
/** Error indicating the account is currently inactive. */
public class InactiveAccountException extends Exception {
private static final long serialVersionUID = 1L;
public static final String MESSAGE = "Account Inactive: ";
public InactiveAccountException(String who) {
super(MESSAGE + who);
}
}

View File

@ -46,6 +46,8 @@ public interface GerritConstants extends Constants {
String nameAlreadyUsedBody(); String nameAlreadyUsedBody();
String noSuchAccountTitle(); String noSuchAccountTitle();
String inactiveAccountBody();
String menuAll(); String menuAll();
String menuAllOpen(); String menuAllOpen();
String menuAllMerged(); String menuAllMerged();

View File

@ -29,6 +29,8 @@ notFoundBody = The page you requested was not found.
nameAlreadyUsedBody = The name is already in use. nameAlreadyUsedBody = The name is already in use.
noSuchAccountTitle = Code Review - Unknown User noSuchAccountTitle = Code Review - Unknown User
inactiveAccountBody = This user is currently inactive.
menuAll = All menuAll = All
menuAllOpen = Open menuAllOpen = Open
menuAllMerged = Merged menuAllMerged = Merged

View File

@ -188,6 +188,10 @@ public class ApprovalTable extends Composite {
r.append(Util.M.accountNotFound(e.getName())); r.append(Util.M.accountNotFound(e.getName()));
break; break;
case ACCOUNT_INACTIVE:
r.append(Util.M.accountInactive(e.getName()));
break;
case CHANGE_NOT_VISIBLE: case CHANGE_NOT_VISIBLE:
r.append(Util.M.changeNotVisibleTo(e.getName())); r.append(Util.M.changeNotVisibleTo(e.getName()));
break; break;

View File

@ -47,6 +47,7 @@ public interface ChangeMessages extends Messages {
String changeQueryPageTitle(String query); String changeQueryPageTitle(String query);
String accountNotFound(String who); String accountNotFound(String who);
String accountInactive(String who);
String changeNotVisibleTo(String who); String changeNotVisibleTo(String who);
String anonymousDownload(String protocol); String anonymousDownload(String protocol);

View File

@ -28,6 +28,7 @@ changeQueryWindowTitle = {0}
changeQueryPageTitle = Search for {0} changeQueryPageTitle = Search for {0}
accountNotFound = {0} is not a registered user. accountNotFound = {0} is not a registered user.
accountInactive = {0} is not an active user.
changeNotVisibleTo = {0} cannot access the change. changeNotVisibleTo = {0} cannot access the change.
anonymousDownload = Anonymous {0} anonymousDownload = Anonymous {0}

View File

@ -17,6 +17,7 @@ package com.google.gerrit.client.rpc;
import com.google.gerrit.client.ErrorDialog; import com.google.gerrit.client.ErrorDialog;
import com.google.gerrit.client.Gerrit; import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.NotSignedInDialog; import com.google.gerrit.client.NotSignedInDialog;
import com.google.gerrit.common.errors.InactiveAccountException;
import com.google.gerrit.common.errors.NameAlreadyUsedException; import com.google.gerrit.common.errors.NameAlreadyUsedException;
import com.google.gerrit.common.errors.NoSuchAccountException; import com.google.gerrit.common.errors.NoSuchAccountException;
import com.google.gerrit.common.errors.NoSuchEntityException; import com.google.gerrit.common.errors.NoSuchEntityException;
@ -37,6 +38,9 @@ public abstract class GerritCallback<T> implements AsyncCallback<T> {
} else if (isNoSuchEntity(caught)) { } else if (isNoSuchEntity(caught)) {
new ErrorDialog(Gerrit.C.notFoundBody()).center(); new ErrorDialog(Gerrit.C.notFoundBody()).center();
} else if (isInactiveAccount(caught)) {
new ErrorDialog(Gerrit.C.inactiveAccountBody()).center();
} else if (isNoSuchAccount(caught)) { } else if (isNoSuchAccount(caught)) {
final String msg = caught.getMessage(); final String msg = caught.getMessage();
final String who = msg.substring(NoSuchAccountException.MESSAGE.length()); final String who = msg.substring(NoSuchAccountException.MESSAGE.length());
@ -71,6 +75,11 @@ public abstract class GerritCallback<T> implements AsyncCallback<T> {
&& caught.getMessage().equals(NoSuchEntityException.MESSAGE); && caught.getMessage().equals(NoSuchEntityException.MESSAGE);
} }
protected static boolean isInactiveAccount(final Throwable caught) {
return caught instanceof RemoteJsonException
&& caught.getMessage().startsWith(InactiveAccountException.MESSAGE);
}
private static boolean isNoSuchAccount(final Throwable caught) { private static boolean isNoSuchAccount(final Throwable caught) {
return caught instanceof RemoteJsonException return caught instanceof RemoteJsonException
&& caught.getMessage().startsWith(NoSuchAccountException.MESSAGE); && caught.getMessage().startsWith(NoSuchAccountException.MESSAGE);

View File

@ -30,7 +30,8 @@ public class AccountSuggestOracle extends HighlightSuggestOracle {
public void onRequestSuggestions(final Request req, final Callback callback) { public void onRequestSuggestions(final Request req, final Callback callback) {
RpcStatus.hide(new Runnable() { RpcStatus.hide(new Runnable() {
public void run() { public void run() {
SuggestUtil.SVC.suggestAccount(req.getQuery(), req.getLimit(), SuggestUtil.SVC.suggestAccount(req.getQuery(), Boolean.TRUE,
req.getLimit(),
new GerritCallback<List<AccountInfo>>() { new GerritCallback<List<AccountInfo>>() {
public void onSuccess(final List<AccountInfo> result) { public void onSuccess(final List<AccountInfo> result) {
final ArrayList<AccountSuggestion> r = final ArrayList<AccountSuggestion> r =

View File

@ -133,7 +133,7 @@ class ProjectDigestFilter implements Filter {
} }
final AccountState who = accountCache.getByUsername(username); final AccountState who = accountCache.getByUsername(username);
if (who == null) { if (who == null || ! who.getAccount().isActive()) {
rsp.sendError(SC_UNAUTHORIZED); rsp.sendError(SC_UNAUTHORIZED);
return false; return false;
} }

View File

@ -33,6 +33,7 @@ import com.google.inject.Provider;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
class SuggestServiceImpl extends BaseServiceImplementation implements class SuggestServiceImpl extends BaseServiceImplementation implements
SuggestService { SuggestService {
@ -74,8 +75,8 @@ class SuggestServiceImpl extends BaseServiceImplementation implements
}); });
} }
public void suggestAccount(final String query, final int limit, public void suggestAccount(final String query, final Boolean active,
final AsyncCallback<List<AccountInfo>> callback) { final int limit, final AsyncCallback<List<AccountInfo>> callback) {
run(callback, new Action<List<AccountInfo>>() { run(callback, new Action<List<AccountInfo>>() {
public List<AccountInfo> run(final ReviewDb db) throws OrmException { public List<AccountInfo> run(final ReviewDb db) throws OrmException {
final String a = query; final String a = query;
@ -86,12 +87,12 @@ class SuggestServiceImpl extends BaseServiceImplementation implements
final LinkedHashMap<Account.Id, AccountInfo> r = final LinkedHashMap<Account.Id, AccountInfo> r =
new LinkedHashMap<Account.Id, AccountInfo>(); new LinkedHashMap<Account.Id, AccountInfo>();
for (final Account p : db.accounts().suggestByFullName(a, b, n)) { for (final Account p : db.accounts().suggestByFullName(a, b, n)) {
r.put(p.getId(), new AccountInfo(p)); addSuggestion(r, p, new AccountInfo(p), active);
} }
if (r.size() < n) { if (r.size() < n) {
for (final Account p : db.accounts().suggestByPreferredEmail(a, b, for (final Account p : db.accounts().suggestByPreferredEmail(a, b,
n - r.size())) { n - r.size())) {
r.put(p.getId(), new AccountInfo(p)); addSuggestion(r, p, new AccountInfo(p), active);
} }
} }
if (r.size() < n) { if (r.size() < n) {
@ -101,7 +102,7 @@ class SuggestServiceImpl extends BaseServiceImplementation implements
final Account p = accountCache.get(e.getAccountId()).getAccount(); final Account p = accountCache.get(e.getAccountId()).getAccount();
final AccountInfo info = new AccountInfo(p); final AccountInfo info = new AccountInfo(p);
info.setPreferredEmail(e.getEmailAddress()); info.setPreferredEmail(e.getEmailAddress());
r.put(e.getAccountId(), info); addSuggestion(r, p, info, active);
} }
} }
} }
@ -110,6 +111,13 @@ class SuggestServiceImpl extends BaseServiceImplementation implements
}); });
} }
private void addSuggestion(Map map, Account account, AccountInfo info,
Boolean active) {
if (active == null || active == account.isActive()) {
map.put(account.getId(), info);
}
}
public void suggestAccountGroup(final String query, final int limit, public void suggestAccountGroup(final String query, final int limit,
final AsyncCallback<List<AccountGroupName>> callback) { final AsyncCallback<List<AccountGroupName>> callback) {
run(callback, new Action<List<AccountGroupName>>() { run(callback, new Action<List<AccountGroupName>>() {

View File

@ -16,6 +16,7 @@ package com.google.gerrit.httpd.rpc.account;
import com.google.gerrit.common.data.GroupAdminService; import com.google.gerrit.common.data.GroupAdminService;
import com.google.gerrit.common.data.GroupDetail; import com.google.gerrit.common.data.GroupDetail;
import com.google.gerrit.common.errors.InactiveAccountException;
import com.google.gerrit.common.errors.NameAlreadyUsedException; import com.google.gerrit.common.errors.NameAlreadyUsedException;
import com.google.gerrit.common.errors.NoSuchAccountException; import com.google.gerrit.common.errors.NoSuchAccountException;
import com.google.gerrit.common.errors.NoSuchEntityException; import com.google.gerrit.common.errors.NoSuchEntityException;
@ -221,6 +222,9 @@ class GroupAdminServiceImpl extends BaseServiceImplementation implements
} }
final Account a = findAccount(nameOrEmail); final Account a = findAccount(nameOrEmail);
if (!a.isActive()) {
throw new Failure(new InactiveAccountException(a.getFullName()));
}
if (!control.canAdd(a.getId())) { if (!control.canAdd(a.getId())) {
throw new Failure(new NoSuchEntityException()); throw new Failure(new NoSuchEntityException());
} }

View File

@ -94,6 +94,11 @@ class AddReviewer extends Handler<ReviewerResult> {
ReviewerResult.Error.Type.ACCOUNT_NOT_FOUND, nameOrEmail)); ReviewerResult.Error.Type.ACCOUNT_NOT_FOUND, nameOrEmail));
continue; continue;
} }
if (!account.isActive()) {
result.addError(new ReviewerResult.Error(
ReviewerResult.Error.Type.ACCOUNT_INACTIVE, nameOrEmail));
continue;
}
final IdentifiedUser user = identifiedUserFactory.create(account.getId()); final IdentifiedUser user = identifiedUserFactory.create(account.getId());
if (!control.forUser(user).isVisible()) { if (!control.forUser(user).isVisible()) {

View File

@ -129,6 +129,10 @@ public final class Account {
@Column(id = 6, name = Column.NONE) @Column(id = 6, name = Column.NONE)
protected AccountGeneralPreferences generalPreferences; protected AccountGeneralPreferences generalPreferences;
/** Is this user active */
@Column(id = 7)
protected boolean inactive;
/** <i>computed</i> the username selected from the identities. */ /** <i>computed</i> the username selected from the identities. */
protected String userName; protected String userName;
@ -198,6 +202,14 @@ public final class Account {
contactFiledOn = new Timestamp(System.currentTimeMillis()); contactFiledOn = new Timestamp(System.currentTimeMillis());
} }
public boolean isActive() {
return ! inactive;
}
public void setActive(boolean active) {
inactive = ! active;
}
/** @return the computed user name for this account */ /** @return the computed user name for this account */
public String getUserName() { public String getUserName() {
return userName; return userName;

View File

@ -100,7 +100,7 @@ public class AccountManager {
* @param who identity of the user, with any details we received about them. * @param who identity of the user, with any details we received about them.
* @return the result of authenticating the user. * @return the result of authenticating the user.
* @throws AccountException the account does not exist, and cannot be created, * @throws AccountException the account does not exist, and cannot be created,
* or exists, but cannot be located. * or exists, but cannot be located, or is inactive.
*/ */
public AuthResult authenticate(AuthRequest who) throws AccountException { public AuthResult authenticate(AuthRequest who) throws AccountException {
who = realm.authenticate(who); who = realm.authenticate(who);
@ -114,9 +114,14 @@ public class AccountManager {
// //
return create(db, who); return create(db, who);
} else { } else { // Account exists
// Account exists, return the identity to the caller.
// Account act = db.accounts().get(id.getAccountId());
if (act == null || !act.isActive()) {
throw new AccountException("Authentication error, account inactive");
}
// return the identity to the caller.
update(db, who, id); update(db, who, id);
return new AuthResult(id.getAccountId(), key, false); return new AuthResult(id.getAccountId(), key, false);
} }

View File

@ -331,7 +331,7 @@ public abstract class OutgoingEmail {
private Address toAddress(final Account.Id id) { private Address toAddress(final Account.Id id) {
final Account a = args.accountCache.get(id).getAccount(); final Account a = args.accountCache.get(id).getAccount();
final String e = a.getPreferredEmail(); final String e = a.getPreferredEmail();
if (e == null) { if (!a.isActive() || e == null) {
return null; return null;
} }
return new Address(a.getFullName(), e); return new Address(a.getFullName(), e);

View File

@ -32,7 +32,7 @@ import java.util.List;
/** A version of the database schema. */ /** A version of the database schema. */
public abstract class SchemaVersion { public abstract class SchemaVersion {
/** The current schema version. */ /** The current schema version. */
private static final Class<? extends SchemaVersion> C = Schema_40.class; private static final Class<? extends SchemaVersion> C = Schema_41.class;
public static class Module extends AbstractModule { public static class Module extends AbstractModule {
@Override @Override

View File

@ -0,0 +1,25 @@
// Copyright (C) 2010 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.schema;
import com.google.inject.Inject;
import com.google.inject.Provider;
public class Schema_41 extends SchemaVersion {
@Inject
Schema_41(Provider<Schema_40> prior) {
super(prior);
}
}

View File

@ -135,6 +135,11 @@ class DatabasePubKeyAuth implements PublickeyAuthenticator {
} }
} }
if (!createUser(sd, key).getAccount().isActive()) {
sd.authenticationError(username, "inactive-account");
return false;
}
return success(username, session, sd, createUser(sd, key)); return success(username, session, sd, createUser(sd, key));
} }