Make external_id primary key of account_external_ids table

Having the primary key be (account_id, external_id) tuple was wrong,
we required exactly one match from an account_id in order to login a
user to their account.  If multiple matches were found we failed with
an exception and denied access.

When we move to a git based backend we really need external_id to be
the proper primary key of this entity, so we should fix it now before
more duplicates show up in real world databases.

Change-Id: Idd44dd67574fedb48e3e0bbd43526e1e67392dfc
Signed-off-by: Shawn O. Pearce <sop@google.com>
Reviewed-by: Grzegorz Kossakowski <grek@google.com>
This commit is contained in:
Shawn O. Pearce
2009-09-09 16:53:00 -07:00
parent 0729e4cc65
commit 70cf025bc4
10 changed files with 85 additions and 97 deletions

View File

@@ -26,29 +26,19 @@ public final class AccountExternalId {
public static final String SCHEME_MAILTO = "mailto:";
public static final String LEGACY_GAE = "Google Account ";
public static class Key extends StringKey<Account.Id> {
public static class Key extends StringKey<com.google.gwtorm.client.Key<?>> {
private static final long serialVersionUID = 1L;
@Column
protected Account.Id accountId;
@Column
protected String externalId;
protected Key() {
accountId = new Account.Id();
}
public Key(final Account.Id a, final String e) {
accountId = a;
public Key(final String e) {
externalId = e;
}
@Override
public Account.Id getParentKey() {
return accountId;
}
@Override
public String get() {
return externalId;
@@ -94,6 +84,9 @@ public final class AccountExternalId {
@Column(name = Column.NONE)
protected Key key;
@Column
protected Account.Id accountId;
@Column(notNull = false)
protected String emailAddress;
@@ -109,9 +102,11 @@ public final class AccountExternalId {
/**
* Create a new binding to an external identity.
*
* @param who the account this binds to.
* @param k the binding key.
*/
public AccountExternalId(final AccountExternalId.Key k) {
public AccountExternalId(final Account.Id who, final AccountExternalId.Key k) {
accountId = who;
key = k;
}
@@ -121,7 +116,7 @@ public final class AccountExternalId {
/** Get local id of this account, to link with in other entities */
public Account.Id getAccountId() {
return key.accountId;
return accountId;
}
public String getExternalId() {

View File

@@ -25,13 +25,10 @@ public interface AccountExternalIdAccess extends
@PrimaryKey("key")
AccountExternalId get(AccountExternalId.Key key) throws OrmException;
@Query("WHERE key.externalId = ? LIMIT 2")
ResultSet<AccountExternalId> byExternal(String id) throws OrmException;
@Query("WHERE key.accountId = ?")
@Query("WHERE accountId = ?")
ResultSet<AccountExternalId> byAccount(Account.Id id) throws OrmException;
@Query("WHERE key.accountId = ? AND emailAddress = ?")
@Query("WHERE accountId = ? AND emailAddress = ?")
ResultSet<AccountExternalId> byAccountEmail(Account.Id id, String email)
throws OrmException;

View File

@@ -56,9 +56,8 @@ public class AccountManager {
try {
final ReviewDb db = schema.open();
try {
final List<AccountExternalId> matches =
db.accountExternalIds().byExternal(externalId).toList();
return !matches.isEmpty();
return db.accountExternalIds().get(
new AccountExternalId.Key(externalId)) != null;
} finally {
db.close();
}
@@ -80,33 +79,19 @@ public class AccountManager {
try {
final ReviewDb db = schema.open();
try {
final List<AccountExternalId> matches =
db.accountExternalIds().byExternal(who.getExternalId()).toList();
switch (matches.size()) {
case 0:
// New account, automatically create and return.
//
return create(db, who);
final AccountExternalId id =
db.accountExternalIds().get(
new AccountExternalId.Key(who.getExternalId()));
if (id == null) {
// New account, automatically create and return.
//
return create(db, who);
case 1: {
// Account exists, return the identity to the caller.
//
final AccountExternalId id = matches.get(0);
update(db, who, id);
return new AuthResult(id.getAccountId(), false);
}
default: {
final StringBuilder r = new StringBuilder();
r.append("Multiple accounts match \"");
r.append(who.getExternalId());
r.append("\":");
for (AccountExternalId e : matches) {
r.append(' ');
r.append(e.getAccountId());
}
throw new AccountException(r.toString());
}
} else {
// Account exists, return the identity to the caller.
//
update(db, who, id);
return new AuthResult(id.getAccountId(), false);
}
} finally {
@@ -270,7 +255,7 @@ public class AccountManager {
private static AccountExternalId createId(final Account.Id newId,
final AuthRequest who) {
final String ext = who.getExternalId();
return new AccountExternalId(new AccountExternalId.Key(newId, ext));
return new AccountExternalId(newId, new AccountExternalId.Key(ext));
}
/**
@@ -286,32 +271,23 @@ public class AccountManager {
try {
final ReviewDb db = schema.open();
try {
final List<AccountExternalId> matches =
db.accountExternalIds().byExternal(who.getExternalId()).toList();
switch (matches.size()) {
case 0: {
final AccountExternalId extId = createId(to, who);
extId.setEmailAddress(who.getEmailAddress());
extId.setLastUsedOn();
db.accountExternalIds().insert(Collections.singleton(extId));
if (who.getEmailAddress() != null) {
byEmailCache.evict(who.getEmailAddress());
byIdCache.evict(to);
}
break;
AccountExternalId extId =
db.accountExternalIds().get(
new AccountExternalId.Key(who.getExternalId()));
if (extId != null) {
if (!extId.getAccountId().equals(to)) {
throw new AccountException("Identity in use by another account");
}
case 1: {
final AccountExternalId extId = matches.get(0);
if (!extId.getAccountId().equals(to)) {
throw new AccountException("Identity already used");
}
update(db, who, extId);
break;
update(db, who, extId);
} else {
extId = createId(to, who);
extId.setEmailAddress(who.getEmailAddress());
extId.setLastUsedOn();
db.accountExternalIds().insert(Collections.singleton(extId));
if (who.getEmailAddress() != null) {
byEmailCache.evict(who.getEmailAddress());
byIdCache.evict(to);
}
default:
throw new AccountException("Identity already used");
}
} finally {

View File

@@ -442,13 +442,10 @@ class LdapRealm implements Realm {
try {
final ReviewDb db = schema.open();
try {
final List<AccountExternalId> candidates =
db.accountExternalIds().byExternal(
AccountExternalId.SCHEME_GERRIT + username).toList();
if (candidates.size() == 1) {
return candidates.get(0).getAccountId();
}
return null;
final String id = AccountExternalId.SCHEME_GERRIT + username;
final AccountExternalId extId =
db.accountExternalIds().get(new AccountExternalId.Key(id));
return extId != null ? extId.getAccountId() : null;
} finally {
db.close();
}

View File

@@ -450,7 +450,7 @@ public class ChangeListServiceImpl extends BaseServiceImplementation implements
addAll(result, db.accounts().suggestByFullName(a, b, 10));
for (AccountExternalId extId : db.accountExternalIds()
.suggestByEmailAddress(a, b, 10)) {
result.add(extId.getKey().getParentKey());
result.add(extId.getAccountId());
}
return result;
}

View File

@@ -234,19 +234,20 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
run(callback, new Action<Set<AccountExternalId.Key>>() {
public Set<AccountExternalId.Key> run(final ReviewDb db)
throws OrmException, Failure {
// Don't permit deletes unless they are for our own account
//
final Account.Id me = getAccountId();
for (final AccountExternalId.Key keyId : keys) {
if (!me.equals(keyId.getParentKey()))
throw new Failure(new NoSuchEntityException());
}
// Determine the records we will allow the user to remove.
//
final Account.Id me = getAccountId();
final Map<AccountExternalId.Key, AccountExternalId> all =
db.accountExternalIds()
.toMap(db.accountExternalIds().byAccount(me));
// Don't permit deletes unless they are for our own account
//
for (final AccountExternalId.Key keyId : keys) {
if (!all.containsKey(keyId))
throw new Failure(new NoSuchEntityException());
}
final AccountExternalId mostRecent =
AccountExternalId.mostRecent(all.values());
final Set<AccountExternalId.Key> removed =

View File

@@ -22,10 +22,9 @@ ON accounts (full_name);
-- *********************************************************************
-- AccountExternalIdAccess
-- @PrimaryKey covers: byAccount
-- covers: byExternal
CREATE INDEX account_external_ids_byExt
ON account_external_ids (external_id);
-- covers: byAccount
CREATE INDEX account_external_ids_byAccount
ON account_external_ids (account_id);
-- covers: byEmailAddress, suggestByEmailAddress
CREATE INDEX account_external_ids_byEmail

View File

@@ -57,10 +57,9 @@ ON accounts (full_name);
-- *********************************************************************
-- AccountExternalIdAccess
-- @PrimaryKey covers: byAccount
-- covers: byExternal
CREATE INDEX account_external_ids_byExt
ON account_external_ids (external_id);
-- covers: byAccount
CREATE INDEX account_external_ids_byAccount
ON account_external_ids (account_id);
-- covers: byEmailAddress, suggestByEmailAddress
CREATE INDEX account_external_ids_byEmail

View File

@@ -28,6 +28,18 @@ AND NOT EXISTS (SELECT 1 FROM copy_patch_comments1 r
DROP TEMPORARY TABLE copy_patch_comments1;
DROP TEMPORARY TABLE copy_patch_comments2;
-- account_external_ids
--
DROP INDEX account_external_ids_byExt;
CREATE INDEX account_external_ids_byAccount
ON account_external_ids (account_id);
ALTER TABLE account_external_ids DROP PRIMARY KEY;
ALTER TABLE account_external_ids ADD PRIMARY KEY (external_id);
DROP TABLE patches;
UPDATE schema_version SET version_nbr = 18;

View File

@@ -27,6 +27,18 @@ AND NOT EXISTS (SELECT 1 FROM patch_comments p
AND p.file_name = patch_comments.file_name
AND p.uuid = patch_comments.parent_uuid);
-- account_external_ids
--
DROP INDEX account_external_ids_byExt;
CREATE INDEX account_external_ids_byAccount
ON account_external_ids (account_id);
ALTER TABLE account_external_ids DROP CONSTRAINT account_external_ids_pkey;
ALTER TABLE account_external_ids ADD PRIMARY KEY (external_id);
DROP TABLE patches;
UPDATE schema_version SET version_nbr = 18;