Move sshUserName from Account to AccountExternalId

We remove the secondary unique column sshUserName and store it in
the AccountExternalId entity instead.  This change is necessary to
support databases which do not allow mulitiple key attributes for
an entity.

Change-Id: I20076a05f2ea083da6044a4f1ed2f0672e85739a
Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
Shawn O. Pearce 2009-12-31 09:20:17 -08:00
parent 8df7cf7689
commit f2bab2afc9
29 changed files with 484 additions and 171 deletions

View File

@ -16,15 +16,14 @@ package com.google.gerrit.common.errors;
import com.google.gerrit.reviewdb.Account;
/** Error indicating the SSH user name does not match {@link Account#SSH_USER_NAME_PATTERN} pattern. */
public class InvalidSshUserNameException extends Exception {
/** Error indicating the SSH user name does not match {@link Account#USER_NAME_PATTERN} pattern. */
public class InvalidUserNameException extends Exception {
private static final long serialVersionUID = 1L;
public static final String MESSAGE = "Invalid SSH user name.";
public static final String MESSAGE = "Invalid user name.";
public InvalidSshUserNameException() {
public InvalidUserNameException() {
super(MESSAGE);
}
}

View File

@ -340,7 +340,6 @@ class ContactPanelShort extends Composite {
final Account me = Gerrit.getUserAccount();
me.setFullName(result.getFullName());
me.setPreferredEmail(result.getPreferredEmail());
me.setSshUserName(result.getSshUserName());
Gerrit.refreshMenuBar();
if (accountSettings != null) {
accountSettings.display(me);

View File

@ -201,7 +201,12 @@ class ExternalIdPanel extends Composite {
if (k.isScheme(AccountExternalId.SCHEME_GERRIT)) {
// A local user identity should just be itself.
//
return k.getSchemeRest(AccountExternalId.SCHEME_GERRIT);
return k.getSchemeRest();
} else if (k.isScheme(AccountExternalId.SCHEME_USERNAME)) {
// A local user identity should just be itself.
//
return k.getSchemeRest();
} else if (k.isScheme(AccountExternalId.SCHEME_MAILTO)) {
// Describe a mailto address as just its email address, which

View File

@ -14,6 +14,8 @@
package com.google.gerrit.client.account;
import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_USERNAME;
import com.google.gerrit.client.ErrorDialog;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.rpc.GerritCallback;
@ -22,8 +24,9 @@ import com.google.gerrit.client.ui.SmallHeading;
import com.google.gerrit.client.ui.TextSaveButtonListener;
import com.google.gerrit.common.data.SshHostKey;
import com.google.gerrit.common.errors.InvalidSshKeyException;
import com.google.gerrit.common.errors.InvalidSshUserNameException;
import com.google.gerrit.common.errors.InvalidUserNameException;
import com.google.gerrit.reviewdb.Account;
import com.google.gerrit.reviewdb.AccountExternalId;
import com.google.gerrit.reviewdb.AccountSshKey;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Element;
@ -94,9 +97,6 @@ class SshPanel extends Composite {
final FlowPanel body = new FlowPanel();
userNameTxt = new NpTextBox();
if (Gerrit.isSignedIn()) {
userNameTxt.setText(Gerrit.getUserAccount().getSshUserName());
}
userNameTxt.addKeyPressHandler(new SshUserNameValidator());
userNameTxt.addStyleName(Gerrit.RESOURCES.css().sshPanelUsername());
userNameTxt.setVisibleLength(16);
@ -221,7 +221,7 @@ class SshPanel extends Composite {
}
private boolean canEditSshUserName() {
return Gerrit.getConfig().canEdit(Account.FieldName.SSH_USER_NAME);
return Gerrit.getConfig().canEdit(Account.FieldName.USER_NAME);
}
protected void row(final Grid info, final int row, final String name,
@ -247,7 +247,7 @@ class SshPanel extends Composite {
if ("".equals(newName)) {
newName = null;
}
if (newName != null && !newName.matches(Account.SSH_USER_NAME_PATTERN)) {
if (newName != null && !newName.matches(Account.USER_NAME_PATTERN)) {
invalidUserName();
return;
}
@ -255,22 +255,20 @@ class SshPanel extends Composite {
userNameTxt.setEnabled(false);
changeUserName.setEnabled(false);
final String newSshUserName = newName;
Util.ACCOUNT_SEC.changeSshUserName(newSshUserName,
final String newUserName = newName;
Util.ACCOUNT_SEC.changeSshUserName(newUserName,
new GerritCallback<VoidResult>() {
public void onSuccess(final VoidResult result) {
Gerrit.getUserAccount().setUserName(newUserName);
userNameTxt.setEnabled(true);
changeUserName.setEnabled(false);
if (Gerrit.isSignedIn()) {
Gerrit.getUserAccount().setSshUserName(newSshUserName);
}
}
@Override
public void onFailure(final Throwable caught) {
userNameTxt.setEnabled(true);
changeUserName.setEnabled(true);
if (InvalidSshUserNameException.MESSAGE.equals(caught.getMessage())) {
if (InvalidUserNameException.MESSAGE.equals(caught.getMessage())) {
invalidUserName();
} else {
super.onFailure(caught);
@ -429,15 +427,21 @@ class SshPanel extends Composite {
super.onLoad();
userNameTxt.setEnabled(false);
Util.ACCOUNT_SVC.myAccount(new GerritCallback<Account>() {
public void onSuccess(final Account result) {
if (Gerrit.isSignedIn()) {
Gerrit.getUserAccount().setSshUserName(result.getSshUserName());
}
userNameTxt.setText(result.getSshUserName());
userNameTxt.setEnabled(true);
}
});
Util.ACCOUNT_SEC
.myExternalIds(new GerritCallback<List<AccountExternalId>>() {
public void onSuccess(final List<AccountExternalId> result) {
String userName = null;
for (AccountExternalId i : result) {
if (i.isScheme(SCHEME_USERNAME)) {
userName = i.getSchemeRest();
break;
}
}
Gerrit.getUserAccount().setUserName(userName);
userNameTxt.setText(userName);
userNameTxt.setEnabled(true);
}
});
Util.ACCOUNT_SEC.mySshKeys(new GerritCallback<List<AccountSshKey>>() {
public void onSuccess(final List<AccountSshKey> result) {
@ -509,9 +513,9 @@ class SshPanel extends Composite {
final TextBox box = (TextBox) event.getSource();
final String re;
if (box.getCursorPos() == 0)
re = Account.SSH_USER_NAME_PATTERN_FIRST;
re = Account.USER_NAME_PATTERN_FIRST;
else
re = Account.SSH_USER_NAME_PATTERN_REST;
re = Account.USER_NAME_PATTERN_REST;
if (!String.valueOf(code).matches("^" + re + "$")) {
event.preventDefault();
event.stopPropagation();

View File

@ -153,8 +153,8 @@ class PatchSetPanel extends Composite implements OpenHandler<DisclosurePanel> {
} else if (Gerrit.isSignedIn() && Gerrit.getUserAccount() != null
&& Gerrit.getConfig().getSshdAddress() != null
&& Gerrit.getUserAccount().getSshUserName() != null
&& Gerrit.getUserAccount().getSshUserName().length() > 0) {
&& Gerrit.getUserAccount().getUserName() != null
&& Gerrit.getUserAccount().getUserName().length() > 0) {
// The user is signed in and anonymous access isn't allowed.
// Use our SSH daemon URL as its the only way they can get
// to the project (that we know of anyway).
@ -162,7 +162,7 @@ class PatchSetPanel extends Composite implements OpenHandler<DisclosurePanel> {
String sshAddr = Gerrit.getConfig().getSshdAddress();
final StringBuilder r = new StringBuilder();
r.append("git pull ssh://");
r.append(Gerrit.getUserAccount().getSshUserName());
r.append(Gerrit.getUserAccount().getUserName());
r.append("@");
if (sshAddr.startsWith("*:") || "".equals(sshAddr)) {
r.append(Window.Location.getHostName());

View File

@ -28,6 +28,7 @@ import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.RemotePeer;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.ChangeUserName;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.FactoryModule;
@ -130,7 +131,11 @@ public class WebModule extends FactoryModule {
SINGLETON);
bind(GerritConfig.class).toProvider(GerritConfigProvider.class).in(
SINGLETON);
bind(AccountManager.class).in(SINGLETON);
bind(AccountManager.class);
bind(ChangeUserName.CurrentUser.class);
factory(ChangeUserName.Factory.class);
bind(SocketAddress.class).annotatedWith(RemotePeer.class).toProvider(
HttpRemotePeerProvider.class).in(RequestScoped.class);

View File

@ -14,6 +14,8 @@
package com.google.gerrit.httpd.auth.become;
import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_USERNAME;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.httpd.HtmlDomUtil;
import com.google.gerrit.httpd.WebSession;
@ -154,12 +156,21 @@ public class BecomeAnyAccountLoginServlet extends HttpServlet {
return null;
}
private AuthResult auth(final AccountExternalId account) {
if (account != null) {
return new AuthResult(account.getAccountId(), null, false);
}
return null;
}
private AuthResult bySshUserName(final HttpServletResponse rsp,
final String userName) {
try {
final ReviewDb db = schema.open();
try {
return auth(db.accounts().bySshUserName(userName));
AccountExternalId.Key key =
new AccountExternalId.Key(SCHEME_USERNAME, userName);
return auth(db.accountExternalIds().get(key));
} finally {
db.close();
}

View File

@ -191,9 +191,9 @@ class GitWebServlet extends HttpServlet {
}
if (gerritConfig.getSshdAddress() != null) {
String sshAddr = gerritConfig.getSshdAddress();
p.print("if ($ENV{'GERRIT_SSH_USER_NAME'}) {\n");
p.print("if ($ENV{'GERRIT_USER_NAME'}) {\n");
p.print(" push @git_base_url_list, join('', 'ssh://'");
p.print(", $ENV{'GERRIT_SSH_USER_NAME'}");
p.print(", $ENV{'GERRIT_USER_NAME'}");
p.print(", '@'");
if (sshAddr.startsWith("*:") || "".equals(sshAddr)) {
p.print(", $ENV{'SERVER_NAME'}");
@ -452,8 +452,8 @@ class GitWebServlet extends HttpServlet {
String remoteUser = null;
if (project.getCurrentUser() instanceof IdentifiedUser) {
final IdentifiedUser u = (IdentifiedUser) project.getCurrentUser();
final String user = u.getAccount().getSshUserName();
env.set("GERRIT_SSH_USER_NAME", user);
final String user = u.getUserName();
env.set("GERRIT_USER_NAME", user);
if (user != null && !user.isEmpty()) {
remoteUser = user;
} else {

View File

@ -14,6 +14,8 @@
package com.google.gerrit.httpd.rpc;
import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_USERNAME;
import com.google.gerrit.common.data.AccountDashboardInfo;
import com.google.gerrit.common.data.ChangeInfo;
import com.google.gerrit.common.data.ChangeListService;
@ -470,8 +472,12 @@ public class ChangeListServiceImpl extends BaseServiceImplementation implements
Set<Account.Id> result = new HashSet<Account.Id>();
String a = userName;
String b = userName + "\u9fa5";
addAll(result, db.accounts().suggestBySshUserName(a, b, 10));
addAll(result, db.accounts().suggestByFullName(a, b, 10));
for (AccountExternalId extId : db.accountExternalIds().suggestByKey(
new AccountExternalId.Key(SCHEME_USERNAME, a),
new AccountExternalId.Key(SCHEME_USERNAME, b), 10)) {
result.add(extId.getAccountId());
}
for (AccountExternalId extId : db.accountExternalIds()
.suggestByEmailAddress(a, b, 10)) {
result.add(extId.getAccountId());

View File

@ -45,6 +45,15 @@ import java.util.concurrent.Callable;
* operation completed successfully.
*/
public abstract class Handler<T> implements Callable<T> {
public static <T> Handler<T> wrap(final Callable<T> r) {
return new Handler<T>() {
@Override
public T call() throws Exception {
return r.call();
}
};
}
/**
* Run the operation and pass the result to the callback.
*

View File

@ -16,6 +16,7 @@ package com.google.gerrit.httpd.rpc.account;
import com.google.gerrit.httpd.rpc.RpcServletModule;
import com.google.gerrit.httpd.rpc.UiRpcModule;
import com.google.gerrit.server.account.ChangeUserName;
import com.google.gerrit.server.config.FactoryModule;
public class AccountModule extends RpcServletModule {

View File

@ -17,10 +17,10 @@ package com.google.gerrit.httpd.rpc.account;
import com.google.gerrit.common.data.AccountSecurity;
import com.google.gerrit.common.errors.ContactInformationStoreException;
import com.google.gerrit.common.errors.InvalidSshKeyException;
import com.google.gerrit.common.errors.InvalidSshUserNameException;
import com.google.gerrit.common.errors.NameAlreadyUsedException;
import com.google.gerrit.common.errors.NoSuchEntityException;
import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
import com.google.gerrit.httpd.rpc.Handler;
import com.google.gerrit.reviewdb.Account;
import com.google.gerrit.reviewdb.AccountAgreement;
import com.google.gerrit.reviewdb.AccountExternalId;
@ -30,11 +30,13 @@ import com.google.gerrit.reviewdb.ContactInformation;
import com.google.gerrit.reviewdb.ContributorAgreement;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountByEmailCache;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.ChangeUserName;
import com.google.gerrit.server.account.Realm;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.contact.ContactStore;
@ -58,17 +60,14 @@ import java.io.UnsupportedEncodingException;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
class AccountSecurityImpl extends BaseServiceImplementation implements
AccountSecurity {
private static final Pattern SSH_USER_NAME_PATTERN = Pattern.compile(Account.SSH_USER_NAME_PATTERN);
private final Logger log = LoggerFactory.getLogger(getClass());
private final ContactStore contactStore;
private final AuthConfig authConfig;
private final Realm realm;
private final Provider<IdentifiedUser> user;
private final RegisterNewEmailSender.Factory registerNewEmailFactory;
private final SshKeyCache sshKeyCache;
private final AccountByEmailCache byEmailCache;
@ -76,6 +75,7 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
private final AccountManager accountManager;
private final boolean useContactInfo;
private final ChangeUserName.CurrentUser changeUserNameFactory;
private final DeleteExternalIds.Factory deleteExternalIdsFactory;
private final ExternalIdDetailFactory.Factory externalIdDetailFactory;
private final MyGroupsFactory.Factory myGroupsFactory;
@ -83,10 +83,11 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
@Inject
AccountSecurityImpl(final Provider<ReviewDb> schema,
final Provider<CurrentUser> currentUser, final ContactStore cs,
final AuthConfig ac, final Realm r,
final AuthConfig ac, final Realm r, final Provider<IdentifiedUser> u,
final RegisterNewEmailSender.Factory esf, final SshKeyCache skc,
final AccountByEmailCache abec, final AccountCache uac,
final AccountManager am,
final ChangeUserName.CurrentUser changeUserNameFactory,
final DeleteExternalIds.Factory deleteExternalIdsFactory,
final ExternalIdDetailFactory.Factory externalIdDetailFactory,
final MyGroupsFactory.Factory myGroupsFactory) {
@ -94,6 +95,7 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
contactStore = cs;
authConfig = ac;
realm = r;
user = u;
registerNewEmailFactory = esf;
sshKeyCache = skc;
byEmailCache = abec;
@ -102,6 +104,7 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
useContactInfo = contactStore != null && contactStore.isEnabled();
this.changeUserNameFactory = changeUserNameFactory;
this.deleteExternalIdsFactory = deleteExternalIdsFactory;
this.externalIdDetailFactory = externalIdDetailFactory;
this.myGroupsFactory = myGroupsFactory;
@ -110,7 +113,8 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
public void mySshKeys(final AsyncCallback<List<AccountSshKey>> callback) {
run(callback, new Action<List<AccountSshKey>>() {
public List<AccountSshKey> run(ReviewDb db) throws OrmException {
return db.accountSshKeys().byAccount(getAccountId()).toList();
IdentifiedUser u = user.get();
return db.accountSshKeys().byAccount(u.getAccountId()).toList();
}
});
}
@ -120,7 +124,7 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
run(callback, new Action<AccountSshKey>() {
public AccountSshKey run(final ReviewDb db) throws OrmException, Failure {
int max = 0;
final Account.Id me = getAccountId();
final Account.Id me = user.get().getAccountId();
for (final AccountSshKey k : db.accountSshKeys().byAccount(me)) {
max = Math.max(max, k.getKey().get());
}
@ -132,7 +136,7 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
throw new Failure(e);
}
db.accountSshKeys().insert(Collections.singleton(key));
uncacheSshKeys(me);
uncacheSshKeys();
return key;
}
});
@ -142,7 +146,7 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
final AsyncCallback<VoidResult> callback) {
run(callback, new Action<VoidResult>() {
public VoidResult run(final ReviewDb db) throws OrmException, Failure {
final Account.Id me = getAccountId();
final Account.Id me = user.get().getAccountId();
for (final AccountSshKey.Id keyId : ids) {
if (!me.equals(keyId.getParentKey()))
throw new Failure(new NoSuchEntityException());
@ -153,7 +157,7 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
final Transaction txn = db.beginTransaction();
db.accountSshKeys().delete(k, txn);
txn.commit();
uncacheSshKeys(me);
uncacheSshKeys();
}
return VoidResult.INSTANCE;
@ -161,56 +165,18 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
});
}
private void uncacheSshKeys(final Account.Id me) {
uncacheSshKeys(accountCache.get(me).getAccount().getSshUserName());
}
private void uncacheSshKeys(final String userName) {
sshKeyCache.evict(userName);
private void uncacheSshKeys() {
sshKeyCache.evict(user.get().getUserName());
}
@Override
public void changeSshUserName(final String newName,
final AsyncCallback<VoidResult> callback) {
if (!realm.allowsEdit(Account.FieldName.SSH_USER_NAME)) {
if (realm.allowsEdit(Account.FieldName.USER_NAME)) {
Handler.wrap(changeUserNameFactory.create(newName)).to(callback);
} else {
callback.onFailure(new NameAlreadyUsedException());
return;
}
run(callback, new Action<VoidResult>() {
@Override
public VoidResult run(ReviewDb db) throws OrmException, Failure {
final Account me = db.accounts().get(getAccountId());
if (me == null) {
throw new Failure(new NoSuchEntityException());
}
if (newName != null && !SSH_USER_NAME_PATTERN.matcher(newName).matches()) {
throw new Failure(new InvalidSshUserNameException());
}
final Account other;
if (newName != null) {
other = db.accounts().bySshUserName(newName);
} else {
other = null;
}
if (other != null) {
if (other.getId().equals(me.getId())) {
return VoidResult.INSTANCE;
} else {
throw new Failure(new NameAlreadyUsedException());
}
}
final String oldName = me.getSshUserName();
me.setSshUserName(newName);
db.accounts().update(Collections.singleton(me));
uncacheSshKeys(oldName);
uncacheSshKeys(newName);
accountCache.evict(me.getId());
return VoidResult.INSTANCE;
}
});
}
public void myExternalIds(AsyncCallback<List<AccountExternalId>> callback) {
@ -231,7 +197,7 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
final ContactInformation info, final AsyncCallback<Account> callback) {
run(callback, new Action<Account>() {
public Account run(ReviewDb db) throws OrmException, Failure {
final Account me = db.accounts().get(getAccountId());
final Account me = db.accounts().get(user.get().getAccountId());
final String oldEmail = me.getPreferredEmail();
if (realm.allowsEdit(Account.FieldName.FULL_NAME)) {
me.setFullName(name != null && !name.isEmpty() ? name : null);
@ -278,7 +244,8 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
}
final AccountAgreement a =
new AccountAgreement(new AccountAgreement.Key(getAccountId(), id));
new AccountAgreement(new AccountAgreement.Key(user.get()
.getAccountId(), id));
if (cla.isAutoVerify()) {
a.review(AccountAgreement.Status.VERIFIED, null);
}
@ -318,7 +285,8 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
callback.onFailure(new IllegalStateException("Invalid token"));
return;
}
accountManager.link(getAccountId(), AuthRequest.forEmail(newEmail));
accountManager.link(user.get().getAccountId(), AuthRequest
.forEmail(newEmail));
callback.onSuccess(VoidResult.INSTANCE);
} catch (XsrfException e) {
callback.onFailure(e);

View File

@ -14,6 +14,8 @@
package com.google.gerrit.httpd.rpc.account;
import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_USERNAME;
import com.google.gerrit.httpd.WebSession;
import com.google.gerrit.httpd.rpc.Handler;
import com.google.gerrit.reviewdb.AccountExternalId;
@ -58,7 +60,11 @@ class ExternalIdDetailFactory extends Handler<List<AccountExternalId>> {
// establish this web session, and if only if an identity was
// actually used to establish this web session.
//
e.setCanDelete(last != null && !last.equals(e.getKey()));
if (e.isScheme(SCHEME_USERNAME)) {
e.setCanDelete(false);
} else {
e.setCanDelete(last != null && !last.equals(e.getKey()));
}
}
return ids;
}

View File

@ -56,18 +56,18 @@ import java.sql.Timestamp;
*/
public final class Account {
public static enum FieldName {
FULL_NAME, SSH_USER_NAME, REGISTER_NEW_EMAIL;
FULL_NAME, USER_NAME, REGISTER_NEW_EMAIL;
}
public static final String SSH_USER_NAME_PATTERN_FIRST = "[a-zA-Z]";
public static final String SSH_USER_NAME_PATTERN_REST = "[a-zA-Z0-9._-]";
public static final String SSH_USER_NAME_PATTERN_LAST = "[a-zA-Z0-9]";
public static final String USER_NAME_PATTERN_FIRST = "[a-zA-Z]";
public static final String USER_NAME_PATTERN_REST = "[a-zA-Z0-9._-]";
public static final String USER_NAME_PATTERN_LAST = "[a-zA-Z0-9]";
/** Regular expression that {@link #sshUserName} must match. */
public static final String SSH_USER_NAME_PATTERN = "^" + //
SSH_USER_NAME_PATTERN_FIRST + //
SSH_USER_NAME_PATTERN_REST + "*" + //
SSH_USER_NAME_PATTERN_LAST + //
/** Regular expression that {@link #userName} must match. */
public static final String USER_NAME_PATTERN = "^" + //
USER_NAME_PATTERN_FIRST + //
USER_NAME_PATTERN_REST + "*" + //
USER_NAME_PATTERN_LAST + //
"$";
/** Key local to Gerrit to identify a user. */
@ -117,18 +117,17 @@ public final class Account {
@Column(id = 4, notNull = false)
protected String preferredEmail;
/** Username to authenticate as through SSH connections. */
@Column(id = 5, notNull = false)
protected String sshUserName;
/** When did the user last give us contact information? Null if never. */
@Column(id = 6, notNull = false)
@Column(id = 5, notNull = false)
protected Timestamp contactFiledOn;
/** This user's preferences */
@Column(id = 7, name = Column.NONE)
@Column(id = 6, name = Column.NONE)
protected AccountGeneralPreferences generalPreferences;
/** <i>computed</i> the username selected from the identities. */
protected String userName;
protected Account() {
}
@ -170,16 +169,6 @@ public final class Account {
preferredEmail = addr;
}
/** Get the name the user logins as through SSH. */
public String getSshUserName() {
return sshUserName;
}
/** Set the name the user logins as through SSH. */
public void setSshUserName(final String name) {
sshUserName = name;
}
/** Get the date and time the user first registered. */
public Timestamp getRegisteredOn() {
return registeredOn;
@ -204,4 +193,14 @@ public final class Account {
public void setContactFiled() {
contactFiledOn = new Timestamp(System.currentTimeMillis());
}
/** @return the computed user name for this account */
public String getUserName() {
return userName;
}
/** Update the computed user name property. */
public void setUserName(final String userName) {
this.userName = userName;
}
}

View File

@ -19,7 +19,6 @@ import com.google.gwtorm.client.OrmException;
import com.google.gwtorm.client.PrimaryKey;
import com.google.gwtorm.client.Query;
import com.google.gwtorm.client.ResultSet;
import com.google.gwtorm.client.SecondaryKey;
/** Access interface for {@link Account}. */
public interface AccountAccess extends Access<Account, Account.Id> {
@ -30,9 +29,6 @@ public interface AccountAccess extends Access<Account, Account.Id> {
@Query("WHERE preferredEmail = ? LIMIT 2")
ResultSet<Account> byPreferredEmail(String email) throws OrmException;
@SecondaryKey("sshUserName")
Account bySshUserName(String userName) throws OrmException;
@Query("WHERE fullName = ? LIMIT 2")
ResultSet<Account> byFullName(String name) throws OrmException;
@ -44,10 +40,6 @@ public interface AccountAccess extends Access<Account, Account.Id> {
ResultSet<Account> suggestByPreferredEmail(String nameA, String nameB,
int limit) throws OrmException;
@Query("WHERE sshUserName >= ? AND sshUserName <= ? ORDER BY sshUserName LIMIT ?")
ResultSet<Account> suggestBySshUserName(String nameA, String nameB, int limit)
throws OrmException;
@Query("LIMIT 1")
ResultSet<Account> anyAccounts() throws OrmException;
}

View File

@ -19,9 +19,24 @@ import com.google.gwtorm.client.StringKey;
/** Association of an external account identifier to a local {@link Account}. */
public final class AccountExternalId {
/**
* Scheme used for {@link AuthType#LDAP}, {@link AuthType#HTTP}, and
* {@link AuthType#HTTP_LDAP} usernames.
* <p>
* The name {@code gerrit:} was a very poor choice.
*/
public static final String SCHEME_GERRIT = "gerrit:";
/** Scheme used for randomly created identities constructed by a UUID. */
public static final String SCHEME_UUID = "uuid:";
/** Scheme used to represent only an email address. */
public static final String SCHEME_MAILTO = "mailto:";
/** Scheme for the username used to authenticate an account, e.g. over SSH. */
public static final String SCHEME_USERNAME = "username:";
/** Very old scheme from Gerrit Code Review 1.x imports. */
public static final String LEGACY_GAE = "Google Account ";
public static class Key extends StringKey<com.google.gwtorm.client.Key<?>> {
@ -33,6 +48,13 @@ public final class AccountExternalId {
protected Key() {
}
public Key(String scheme, final String identity) {
if (!scheme.endsWith(":")) {
scheme += ":";
}
externalId = scheme + identity;
}
public Key(final String e) {
externalId = e;
}
@ -103,8 +125,10 @@ public final class AccountExternalId {
return id != null && id.startsWith(scheme);
}
public String getSchemeRest(final String scheme) {
return isScheme(scheme) ? getExternalId().substring(scheme.length()) : null;
public String getSchemeRest() {
String id = getExternalId();
int c = id.indexOf(':');
return 0 < c ? id.substring(c + 1) : null;
}
public boolean isTrusted() {

View File

@ -25,6 +25,10 @@ public interface AccountExternalIdAccess extends
@PrimaryKey("key")
AccountExternalId get(AccountExternalId.Key key) throws OrmException;
@Query("WHERE key >= ? AND key <= ? ORDER BY key LIMIT ?")
ResultSet<AccountExternalId> suggestByKey(AccountExternalId.Key keyA,
AccountExternalId.Key keyB, int limit) throws OrmException;
@Query("WHERE accountId = ?")
ResultSet<AccountExternalId> byAccount(Account.Id id) throws OrmException;

View File

@ -27,7 +27,4 @@ public interface AccountSshKeyAccess extends
@Query("WHERE id.accountId = ?")
ResultSet<AccountSshKey> byAccount(Account.Id id) throws OrmException;
@Query("WHERE id.accountId = ? AND valid = true")
ResultSet<AccountSshKey> valid(Account.Id id) throws OrmException;
}

View File

@ -159,6 +159,11 @@ public class IdentifiedUser extends CurrentUser {
return accountId;
}
/** @return the user's user name; null if one has not been selected/assigned. */
public String getUserName() {
return state().getUserName();
}
public Account getAccount() {
return state().getAccount();
}
@ -220,7 +225,7 @@ public class IdentifiedUser extends CurrentUser {
name = "Anonymous Coward";
}
String user = ua.getSshUserName();
String user = getUserName();
if (user == null) {
user = "";
}
@ -253,7 +258,7 @@ public class IdentifiedUser extends CurrentUser {
// don't leak an address the user may have given us, but doesn't
// necessarily want to publish through Git records.
//
String user = ua.getSshUserName();
String user = getUserName();
if (user == null || user.isEmpty()) {
user = "account-" + ua.getId().toString();
}

View File

@ -15,12 +15,15 @@
package com.google.gerrit.server.account;
import com.google.gerrit.common.auth.openid.OpenIdUrls;
import com.google.gerrit.common.errors.InvalidUserNameException;
import com.google.gerrit.common.errors.NameAlreadyUsedException;
import com.google.gerrit.reviewdb.Account;
import com.google.gerrit.reviewdb.AccountExternalId;
import com.google.gerrit.reviewdb.AccountGroup;
import com.google.gerrit.reviewdb.AccountGroupMember;
import com.google.gerrit.reviewdb.AccountGroupMemberAudit;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gwtorm.client.OrmException;
import com.google.gwtorm.client.SchemaFactory;
@ -28,6 +31,9 @@ import com.google.gwtorm.client.Transaction;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -36,23 +42,31 @@ import java.util.concurrent.atomic.AtomicBoolean;
/** Tracks authentication related details for user accounts. */
@Singleton
public class AccountManager {
private static final Logger log =
LoggerFactory.getLogger(AccountManager.class);
private final SchemaFactory<ReviewDb> schema;
private final AccountCache byIdCache;
private final AccountByEmailCache byEmailCache;
private final AuthConfig authConfig;
private final Realm realm;
private final IdentifiedUser.GenericFactory userFactory;
private final ChangeUserName.Factory changeUserNameFactory;
private final AtomicBoolean firstAccount;
@Inject
AccountManager(final SchemaFactory<ReviewDb> schema,
final AccountCache byIdCache, final AccountByEmailCache byEmailCache,
final AuthConfig authConfig, final Realm accountMapper)
throws OrmException {
final AuthConfig authConfig, final Realm accountMapper,
final IdentifiedUser.GenericFactory userFactory,
final ChangeUserName.Factory changeUserNameFactory) throws OrmException {
this.schema = schema;
this.byIdCache = byIdCache;
this.byEmailCache = byEmailCache;
this.authConfig = authConfig;
this.realm = accountMapper;
this.userFactory = userFactory;
this.changeUserNameFactory = changeUserNameFactory;
firstAccount = new AtomicBoolean();
final ReviewDb db = schema.open();
@ -144,10 +158,10 @@ public class AccountManager {
updateAccount = true;
account.setFullName(who.getDisplayName());
}
if (!realm.allowsEdit(Account.FieldName.SSH_USER_NAME)
&& !eq(account.getSshUserName(), who.getSshUserName())) {
if (!realm.allowsEdit(Account.FieldName.USER_NAME)
&& !eq(account.getUserName(), who.getUserName())) {
updateAccount = true;
account.setSshUserName(who.getSshUserName());
account.setUserName(who.getUserName());
}
db.accountExternalIds().update(Collections.singleton(extId), txn);
@ -246,13 +260,6 @@ public class AccountManager {
account.setFullName(who.getDisplayName());
account.setPreferredEmail(extId.getEmailAddress());
if (who.getSshUserName() != null
&& db.accounts().bySshUserName(who.getSshUserName()) == null) {
// Only set if the name hasn't been used yet, but was given to us.
//
account.setSshUserName(who.getSshUserName());
}
final Transaction txn = db.beginTransaction();
db.accounts().insert(Collections.singleton(account), txn);
db.accountExternalIds().insert(Collections.singleton(extId), txn);
@ -272,6 +279,23 @@ public class AccountManager {
txn.commit();
if (who.getUserName() != null) {
// Only set if the name hasn't been used yet, but was given to us.
//
IdentifiedUser user = userFactory.create(newId);
try {
changeUserNameFactory.create(db, user, who.getUserName()).call();
} catch (NameAlreadyUsedException e) {
log.error("Cannot assign user name \"" + who.getUserName()
+ "\" to account " + newId + "; name already in use.");
} catch (InvalidUserNameException e) {
log.error("Cannot assign user name \"" + who.getUserName()
+ "\" to account " + newId + "; name does not conform.");
} catch (OrmException e) {
log.error("Cannot assign user name", e);
}
}
byEmailCache.evict(account.getPreferredEmail());
realm.onCreateAccount(who, account);
return new AuthResult(newId, extId.getKey(), true);

View File

@ -14,6 +14,8 @@
package com.google.gerrit.server.account;
import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_USERNAME;
import com.google.gerrit.reviewdb.Account;
import com.google.gerrit.reviewdb.AccountExternalId;
import com.google.gerrit.reviewdb.AccountGroup;
@ -33,6 +35,7 @@ public class AccountState {
this.account = account;
this.internalGroups = actualGroups;
this.externalIds = externalIds;
this.account.setUserName(getUserName(externalIds));
}
/** Get the cached account metadata. */
@ -40,6 +43,16 @@ public class AccountState {
return account;
}
/**
* Get the username, if one has been declared for this user.
* <p>
* The username is the {@link AccountExternalId} using the scheme
* {@link AccountExternalId#SCHEME_USERNAME}.
*/
public String getUserName() {
return account.getUserName();
}
/**
* All email addresses registered to this account.
* <p>
@ -68,4 +81,13 @@ public class AccountState {
public Set<AccountGroup.Id> getInternalGroups() {
return internalGroups;
}
private static String getUserName(Collection<AccountExternalId> ids) {
for (AccountExternalId id : ids) {
if (id.isScheme(SCHEME_USERNAME)) {
return id.getSchemeRest();
}
}
return null;
}
}

View File

@ -17,6 +17,8 @@ package com.google.gerrit.server.account;
import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_GERRIT;
import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_MAILTO;
import com.google.gerrit.reviewdb.AccountExternalId;
/**
* Information for {@link AccountManager#authenticate(AuthRequest)}.
* <p>
@ -29,9 +31,10 @@ import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_MAILTO;
public class AuthRequest {
/** Create a request for a local username, such as from LDAP. */
public static AuthRequest forUser(final String username) {
final AuthRequest r;
r = new AuthRequest(SCHEME_GERRIT + username);
r.setSshUserName(username);
final AccountExternalId.Key i =
new AccountExternalId.Key(SCHEME_GERRIT, username);
final AuthRequest r = new AuthRequest(i.get());
r.setUserName(username);
return r;
}
@ -42,8 +45,9 @@ public class AuthRequest {
* an existing user account.
*/
public static AuthRequest forEmail(final String email) {
final AuthRequest r;
r = new AuthRequest(SCHEME_MAILTO + email);
final AccountExternalId.Key i =
new AccountExternalId.Key(SCHEME_MAILTO, email);
final AuthRequest r = new AuthRequest(i.get());
r.setEmailAddress(email);
return r;
}
@ -52,7 +56,7 @@ public class AuthRequest {
private String password;
private String displayName;
private String emailAddress;
private String sshUserName;
private String userName;
public AuthRequest(final String externalId) {
this.externalId = externalId;
@ -97,11 +101,11 @@ public class AuthRequest {
emailAddress = email != null && email.length() > 0 ? email : null;
}
public String getSshUserName() {
return sshUserName;
public String getUserName() {
return userName;
}
public void setSshUserName(final String user) {
sshUserName = user;
public void setUserName(final String user) {
userName = user;
}
}

View File

@ -0,0 +1,142 @@
// Copyright (C) 2009 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.reviewdb.AccountExternalId.SCHEME_USERNAME;
import com.google.gerrit.common.errors.InvalidUserNameException;
import com.google.gerrit.common.errors.NameAlreadyUsedException;
import com.google.gerrit.reviewdb.Account;
import com.google.gerrit.reviewdb.AccountExternalId;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.ssh.SshKeyCache;
import com.google.gwtjsonrpc.client.VoidResult;
import com.google.gwtorm.client.OrmDuplicateKeyException;
import com.google.gwtorm.client.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.Callable;
import java.util.regex.Pattern;
/** Operation to change the username of an account. */
public class ChangeUserName implements Callable<VoidResult> {
private static final Pattern USER_NAME_PATTERN =
Pattern.compile(Account.USER_NAME_PATTERN);
/** Factory to change the username for the current user. */
public static class CurrentUser {
private final Factory factory;
private final Provider<ReviewDb> db;
private final Provider<IdentifiedUser> user;
@Inject
CurrentUser(Factory factory, Provider<ReviewDb> db,
Provider<IdentifiedUser> user) {
this.factory = factory;
this.db = db;
this.user = user;
}
public ChangeUserName create(String newUsername) {
return factory.create(db.get(), user.get(), newUsername);
}
}
/** Generic factory to change any user's username. */
public interface Factory {
ChangeUserName create(ReviewDb db, IdentifiedUser user, String newUsername);
}
private final AccountCache accountCache;
private final SshKeyCache sshKeyCache;
private final ReviewDb db;
private final IdentifiedUser user;
private final String newUsername;
@Inject
ChangeUserName(final AccountCache accountCache,
final SshKeyCache sshKeyCache,
@Assisted final ReviewDb db, @Assisted final IdentifiedUser user,
@Assisted final String newUsername) {
this.accountCache = accountCache;
this.sshKeyCache = sshKeyCache;
this.db = db;
this.user = user;
this.newUsername = newUsername;
}
public VoidResult call() throws OrmException, NameAlreadyUsedException,
InvalidUserNameException {
final Collection<AccountExternalId> old = old();
if (newUsername != null && !newUsername.isEmpty()) {
if (!USER_NAME_PATTERN.matcher(newUsername).matches()) {
throw new InvalidUserNameException();
}
final AccountExternalId.Key key =
new AccountExternalId.Key(SCHEME_USERNAME, newUsername);
try {
final AccountExternalId id =
new AccountExternalId(user.getAccountId(), key);
db.accountExternalIds().insert(Collections.singleton(id));
} catch (OrmDuplicateKeyException dupeErr) {
// If we are using this identity, don't report the exception.
//
AccountExternalId other = db.accountExternalIds().get(key);
if (other != null && other.getAccountId().equals(user.getAccountId())) {
return VoidResult.INSTANCE;
}
// Otherwise, someone else has this identity.
//
throw new NameAlreadyUsedException();
}
}
// If we have any older user names, remove them.
//
if (!old.isEmpty()) {
db.accountExternalIds().delete(old);
for (AccountExternalId i : old) {
sshKeyCache.evict(i.getSchemeRest());
}
}
accountCache.evict(user.getAccountId());
sshKeyCache.evict(newUsername);
return VoidResult.INSTANCE;
}
private Collection<AccountExternalId> old() throws OrmException {
final Collection<AccountExternalId> r = new ArrayList<AccountExternalId>(1);
for (AccountExternalId i : db.accountExternalIds().byAccount(
user.getAccountId())) {
if (i.isScheme(SCHEME_USERNAME)) {
r.add(i);
}
}
return r;
}
}

View File

@ -14,6 +14,8 @@
package com.google.gerrit.server.auth.ldap;
import static com.google.gerrit.reviewdb.AccountExternalId.*;
import com.google.gerrit.reviewdb.Account;
import com.google.gerrit.reviewdb.AccountExternalId;
import com.google.gerrit.reviewdb.AccountGroup;
@ -278,7 +280,7 @@ class LdapRealm implements Realm {
case FULL_NAME:
return accountFullName == null; // only if not obtained from LDAP
case SSH_USER_NAME:
case USER_NAME:
return accountSshUserName == null; // only if not obtained from LDAP
default:
@ -317,7 +319,7 @@ class LdapRealm implements Realm {
}
who.setDisplayName(apply(accountFullName, m));
who.setSshUserName(apply(accountSshUserName, m));
who.setUserName(apply(accountSshUserName, m));
if (accountEmailAddress != null) {
who.setEmailAddress(apply(accountEmailAddress, m));
@ -457,7 +459,7 @@ class LdapRealm implements Realm {
private static String findId(final Collection<AccountExternalId> ids) {
for (final AccountExternalId i : ids) {
if (i.isScheme(AccountExternalId.SCHEME_GERRIT)) {
return i.getSchemeRest(AccountExternalId.SCHEME_GERRIT);
return i.getSchemeRest();
}
}
return null;
@ -505,9 +507,9 @@ class LdapRealm implements Realm {
try {
final ReviewDb db = schema.open();
try {
final String id = AccountExternalId.SCHEME_GERRIT + username;
final AccountExternalId extId =
db.accountExternalIds().get(new AccountExternalId.Key(id));
db.accountExternalIds().get(
new AccountExternalId.Key(SCHEME_GERRIT, username));
return extId != null ? extId.getAccountId() : null;
} finally {
db.close();

View File

@ -169,6 +169,12 @@ public class AuthConfig {
return true;
}
if (id.isScheme(AccountExternalId.SCHEME_USERNAME)) {
// We can trust their username, its local to our server only.
//
return true;
}
for (final String p : trusted) {
if (matches(p, id)) {
return true;

View File

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

View File

@ -0,0 +1,73 @@
// Copyright (C) 2009 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 static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_USERNAME;
import com.google.gerrit.reviewdb.Account;
import com.google.gerrit.reviewdb.AccountExternalId;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.reviewdb.AccountExternalId.Key;
import com.google.gwtorm.client.OrmException;
import com.google.gwtorm.client.Transaction;
import com.google.gwtorm.jdbc.JdbcSchema;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
class Schema_22 extends SchemaVersion {
@Inject
Schema_22(Provider<Schema_21> prior) {
super(prior);
}
@Override
protected void migrateData(ReviewDb db) throws OrmException, SQLException {
Collection<AccountExternalId> ids = new ArrayList<AccountExternalId>();
Statement queryStmt = ((JdbcSchema) db).getConnection().createStatement();
try {
ResultSet results =
queryStmt.executeQuery(//
"SELECT account_id, ssh_user_name"
+ " FROM accounts" //
+ " WHERE ssh_user_name IS NOT NULL"
+ " AND ssh_user_name <> ''");
while (results.next()) {
final int accountId = results.getInt(1);
final String userName = results.getString(2);
final Account.Id account = new Account.Id(accountId);
final AccountExternalId.Key key = toKey(userName);
ids.add(new AccountExternalId(account, key));
}
} finally {
queryStmt.close();
}
if (!ids.isEmpty()) {
Transaction t = db.beginTransaction();
db.accountExternalIds().insert(ids, t);
t.commit();
}
}
private Key toKey(final String userName) {
return new AccountExternalId.Key(SCHEME_USERNAME, userName);
}
}

View File

@ -14,8 +14,10 @@
package com.google.gerrit.sshd;
import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_USERNAME;
import com.google.gerrit.common.errors.InvalidSshKeyException;
import com.google.gerrit.reviewdb.Account;
import com.google.gerrit.reviewdb.AccountExternalId;
import com.google.gerrit.reviewdb.AccountSshKey;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.server.cache.Cache;
@ -122,14 +124,18 @@ public class SshKeyCacheImpl implements SshKeyCache {
throws Exception {
final ReviewDb db = schema.open();
try {
final Account user = db.accounts().bySshUserName(username);
final AccountExternalId.Key key =
new AccountExternalId.Key(SCHEME_USERNAME, username);
final AccountExternalId user = db.accountExternalIds().get(key);
if (user == null) {
return NO_SUCH_USER;
}
final List<SshKeyCacheEntry> kl = new ArrayList<SshKeyCacheEntry>(4);
for (final AccountSshKey k : db.accountSshKeys().valid(user.getId())) {
add(db, kl, k);
for (AccountSshKey k : db.accountSshKeys().byAccount(user.getAccountId())) {
if (k.isValid()) {
add(db, kl, k);
}
}
if (kl.isEmpty()) {
return NO_KEYS;

View File

@ -182,7 +182,7 @@ class SshLog implements LifecycleListener {
);
event.setProperty(P_SESSION, id(s.getAttribute(SshUtil.SESSION_ID)));
event.setProperty(P_USER_NAME, u.getAccount().getSshUserName());
event.setProperty(P_USER_NAME, u.getUserName());
event.setProperty(P_ACCOUNT_ID, "a/" + u.getAccountId().toString());
return event;