Automatically upgrade legacy Gerrit 1 accounts to Gerrit 2 on first login

If the OpenId provider was the Google Account provider and the user
already exists with an external id of "GoogleAccount/$email" then
they were imported off a Gerrit 1 schema and we can trust that the
identity is still the same individual.

We also now support more than one OpenId string per account; this
may be necessary to allow users who have more than one OpenId to
link all of their accounts together within Gerrit.  Currently we
do not have a way for users to manage this linkage on their own,
but we might support it in the future by emailing tokens or some
other sort of method.

Keeping the legacy 'GoogleAccount/$email' identity really helps
during testing, as Google's OpenID provider creates different key
strings for each hostname:port combination it sees for an account.

By allowing multiple external strings and keeping our special legacy
entry we can permit developers to bind back to their own accounts
no matter how they started the development server, or no matter
what URL they used to access it.

Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
Shawn O. Pearce
2008-12-02 11:40:09 -08:00
parent 3b14cd7b70
commit dd8543d03d
7 changed files with 186 additions and 50 deletions

View File

@@ -16,8 +16,7 @@ INSERT INTO accounts
contact_address,
contact_country,
contact_phone_nbr,
contact_fax_nbr,
openid_identity
contact_fax_nbr
) SELECT
nextval('account_id'),
a.created,
@@ -26,10 +25,18 @@ INSERT INTO accounts
a.mailing_address,
a.mailing_address_country,
a.phone_number,
a.fax_number,
'GoogleAccount<' || a.user_email || '>'
a.fax_number
FROM gerrit1.accounts a;
DELETE FROM account_external_ids;
INSERT INTO account_external_ids
(account_id,
external_id) SELECT
l.account_id,
'GoogleAccount/' || a.user_email
FROM gerrit1.accounts a, accounts l
WHERE l.preferred_email = a.user_email;
DELETE FROM contributor_agreements;
INSERT INTO contributor_agreements
(active,
@@ -78,7 +85,7 @@ INSERT INTO account_agreements
r.account_id,
a.cla_comments,
(SELECT m.account_id FROM accounts m
WHERE m.openid_identity = 'GoogleAccount<' || a.cla_verified_by || '>'),
WHERE m.preferred_email = a.cla_verified_by),
a.cla_verified_timestamp,
i.id
FROM contributor_agreements i,
@@ -86,7 +93,7 @@ INSERT INTO account_agreements
accounts r
WHERE i.short_name = 'Individual'
AND a.individual_cla_version = 1
AND r.openid_identity = 'GoogleAccount<' || a.user_email || '>';
AND r.preferred_email = a.user_email;
INSERT INTO account_agreements
(accepted_on,
status,
@@ -103,7 +110,7 @@ INSERT INTO account_agreements
r.account_id,
a.cla_comments,
(SELECT m.account_id FROM accounts m
WHERE m.openid_identity = 'GoogleAccount<' || a.cla_verified_by || '>'),
WHERE m.preferred_email = a.cla_verified_by),
a.cla_verified_timestamp,
i.id
FROM contributor_agreements i,
@@ -112,7 +119,7 @@ INSERT INTO account_agreements
WHERE i.short_name = 'Corporate'
AND a.individual_cla_version = 0
AND a.cla_verified = 'Y'
AND r.openid_identity = 'GoogleAccount<' || a.user_email || '>';
AND r.preferred_email = a.user_email;
DELETE FROM account_groups;
INSERT INTO account_groups
@@ -137,7 +144,7 @@ INSERT INTO account_group_members
gerrit1.account_group_users o
WHERE
o.group_name = g.name
AND a.openid_identity = 'GoogleAccount<' || o.email || '>';
AND a.preferred_email = o.email;
UPDATE account_group_members SET owner = 'Y'
WHERE group_id = (SELECT group_id FROM account_groups
WHERE name = 'admin');
@@ -173,7 +180,7 @@ INSERT INTO project_lead_accounts
accounts a,
gerrit1.project_owner_users o
WHERE p.project_id = o.project_id
AND a.openid_identity = 'GoogleAccount<' || o.email || '>';
AND a.preferred_email = o.email;
DELETE FROM project_lead_groups;
INSERT INTO project_lead_groups
@@ -217,7 +224,7 @@ INSERT INTO changes
gerrit1.projects p,
gerrit1.branches b
WHERE
a.openid_identity = 'GoogleAccount<' || c.owner || '>'
a.preferred_email = c.owner
AND p.gae_key = c.dest_project_key
AND b.gae_key = c.dest_branch_key
;
@@ -317,7 +324,7 @@ INSERT INTO patch_comments
WHERE o_p.patchset_key = o_ps.gae_key
AND o_ps.change_key = o_c.gae_key
AND o_p.gae_key = c.patch_key
AND a.openid_identity = 'GoogleAccount<' || c.author || '>';
AND a.preferred_email = c.author;
DELETE FROM change_approvals;
INSERT INTO change_approvals
@@ -335,7 +342,7 @@ INSERT INTO change_approvals
WHERE
s.verified = 'Y'
AND s.change_key = c.gae_key
AND a.openid_identity = 'GoogleAccount<' || s.email || '>';
AND a.preferred_email = s.email;
INSERT INTO change_approvals
(value,
change_id,
@@ -357,7 +364,7 @@ INSERT INTO change_approvals
WHERE
s.lgtm IS NOT NULL
AND s.change_key = c.gae_key
AND a.openid_identity = 'GoogleAccount<' || s.email || '>';
AND a.preferred_email = s.email;
SELECT
(SELECT COUNT(*) FROM gerrit1.accounts) as accounts_g1,

View File

@@ -16,30 +16,11 @@ package com.google.gerrit.client.reviewdb;
import com.google.gwtorm.client.Column;
import com.google.gwtorm.client.IntKey;
import com.google.gwtorm.client.StringKey;
import java.sql.Timestamp;
/** Preferences and information about a single user. */
public final class Account {
/** Globally unique key to identify a user. */
public static class OpenId extends StringKey<com.google.gwtorm.client.Key<?>> {
@Column
protected String openidIdentity;
protected OpenId() {
}
public OpenId(final String id) {
openidIdentity = id;
}
@Override
public String get() {
return openidIdentity;
}
}
/** Key local to Gerrit to identify a user. */
public static class Id extends IntKey<com.google.gwtorm.client.Key<?>> {
@Column
@@ -61,10 +42,6 @@ public final class Account {
@Column
protected Id accountId;
/** Identity from the OpenID provider the user authenticates through. */
@Column
protected OpenId openidIdentity;
/** Date and time the user registered with the review server. */
@Column
protected Timestamp registeredOn;
@@ -87,11 +64,9 @@ public final class Account {
/**
* Create a new account.
*
* @param identity identity assigned by the OpenID provider.
* @param newId unique id, see {@link ReviewDb#nextAccountId()}.
*/
public Account(final Account.OpenId identity, final Account.Id newId) {
openidIdentity = identity;
public Account(final Account.Id newId) {
accountId = newId;
registeredOn = new Timestamp(System.currentTimeMillis());
}

View File

@@ -16,17 +16,12 @@ package com.google.gerrit.client.reviewdb;
import com.google.gwtorm.client.Access;
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.OpenId> {
/** Locate an account by its OpenID provider supplied identifier string */
@PrimaryKey("openidIdentity")
Account byOpenId(Account.OpenId key) throws OrmException;
public interface AccountAccess extends Access<Account, Account.Id> {
/** Locate an account by our locally generated identity. */
@SecondaryKey("accountId")
Account byId(Account.Id key) throws OrmException;

View File

@@ -0,0 +1,72 @@
// Copyright 2008 Google Inc.
//
// 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.client.reviewdb;
import com.google.gwtorm.client.Column;
/** Association of an external account identifier to a local {@link Account}. */
public final class AccountExternalId {
public static class Key implements com.google.gwtorm.client.Key<Account.Id> {
@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;
externalId = e;
}
public Account.Id getParentKey() {
return accountId;
}
@Override
public int hashCode() {
return accountId.hashCode() * 31 + externalId.hashCode();
}
@Override
public boolean equals(final Object o) {
return o instanceof Key && ((Key) o).accountId.equals(accountId)
&& ((Key) o).externalId.equals(externalId);
}
}
@Column(name = Column.NONE)
protected Key key;
protected AccountExternalId() {
}
/**
* Create a new binding to an external identity.
*
* @param k the binding key.
*/
public AccountExternalId(final AccountExternalId.Key k) {
key = k;
}
/** Get local id of this account, to link with in other entities */
public Account.Id getAccountId() {
return key.accountId;
}
}

View File

@@ -0,0 +1,33 @@
// Copyright 2008 Google Inc.
//
// 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.client.reviewdb;
import com.google.gwtorm.client.Access;
import com.google.gwtorm.client.OrmException;
import com.google.gwtorm.client.PrimaryKey;
import com.google.gwtorm.client.Query;
import com.google.gwtorm.client.ResultSet;
public interface AccountExternalIdAccess extends
Access<AccountExternalId, AccountExternalId.Key> {
@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 = ?")
ResultSet<AccountExternalId> byAccount(Account.Id id) throws OrmException;
}

View File

@@ -35,6 +35,9 @@ public interface ReviewDb extends Schema {
@Relation
AccountAccess accounts();
@Relation
AccountExternalIdAccess accountExternalIds();
@Relation
AccountAgreementAccess accountAgreements();

View File

@@ -17,12 +17,15 @@ package com.google.gerrit.server;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.account.SignInResult;
import com.google.gerrit.client.reviewdb.Account;
import com.google.gerrit.client.reviewdb.AccountExternalId;
import com.google.gerrit.client.reviewdb.AccountExternalIdAccess;
import com.google.gerrit.client.reviewdb.ReviewDb;
import com.google.gwt.user.server.rpc.RPCServletUtils;
import com.google.gwtjsonrpc.server.JsonServlet;
import com.google.gwtjsonrpc.server.ValidToken;
import com.google.gwtjsonrpc.server.XsrfException;
import com.google.gwtorm.client.OrmException;
import com.google.gwtorm.client.Transaction;
import com.dyuproject.openid.Constants;
import com.dyuproject.openid.OpenIdContext;
@@ -38,6 +41,7 @@ import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.zip.GZIPOutputStream;
@@ -216,11 +220,31 @@ public class LoginServlet extends HttpServlet {
throws IOException {
Account account = null;
if (user != null) {
final Account.OpenId provId = new Account.OpenId(user.getIdentity());
try {
final ReviewDb d = server.getDatabase().open();
try {
account = d.accounts().byOpenId(provId);
final AccountExternalIdAccess extAccess = d.accountExternalIds();
AccountExternalId acctExt = lookup(extAccess, user.getIdentity());
if (acctExt == null && email != null && isGoogleAccount(user)) {
acctExt = lookup(extAccess, "GoogleAccount/" + email);
if (acctExt != null) {
// Legacy user from Gerrit 1? Attach the OpenID identity.
//
final AccountExternalId openidExt =
new AccountExternalId(new AccountExternalId.Key(acctExt
.getAccountId(), user.getIdentity()));
extAccess.insert(Collections.singleton(openidExt));
acctExt = openidExt;
}
}
if (acctExt != null) {
account = d.accounts().byId(acctExt.getAccountId());
} else {
account = null;
}
if (account != null) {
// Existing user; double check the email is current.
//
@@ -231,9 +255,18 @@ public class LoginServlet extends HttpServlet {
} else {
// New user; create an account entity for them.
//
account = new Account(provId, new Account.Id(d.nextAccountId()));
final Transaction txn = d.beginTransaction();
account = new Account(new Account.Id(d.nextAccountId()));
account.setPreferredEmail(email);
d.accounts().insert(Collections.singleton(account));
acctExt =
new AccountExternalId(new AccountExternalId.Key(
account.getId(), user.getIdentity()));
d.accounts().insert(Collections.singleton(account), txn);
extAccess.insert(Collections.singleton(acctExt), txn);
txn.commit();
}
} finally {
d.close();
@@ -280,6 +313,24 @@ public class LoginServlet extends HttpServlet {
}
}
private static AccountExternalId lookup(
final AccountExternalIdAccess extAccess, final String id)
throws OrmException {
final List<AccountExternalId> extRes = extAccess.byExternal(id).toList();
switch (extRes.size()) {
case 0:
return null;
case 1:
return extRes.get(0);
default:
throw new OrmException("More than one account matches: " + id);
}
}
private static boolean isGoogleAccount(final OpenIdUser user) {
return user.getIdentity().startsWith(GoogleAccountDiscovery.GOOGLE_ACCOUNT);
}
private void modeChkSetCookie(final HttpServletRequest req,
final HttpServletResponse rsp, final boolean isCheck) throws IOException {
final String exp = req.getParameter(Gerrit.ACCOUNT_COOKIE);