Abstract out account creation and simplify sign-on for users
We now do all account management in the AccountManager class, so we don't have code duplication between HTTP and OpenID authentication. This also cleans up GerritCall, moving the account creation logic out of a per-request state object. It just doesn't belong there. In HTTP authentication mode we also correctly store the state of the "#change,42" style anchors during web authentication by using a JavaScript based redirect from an unauthenticated space into the authenticated space, do the authentication, then redirect back to the token. This allows users who have not yet logged into the app to still click on anchor links received by email, and visit the URL. Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
// 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;
|
||||
|
||||
/** An account processing error thrown by {@link AccountManager}. */
|
||||
public class AccountException extends Exception {
|
||||
public AccountException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public AccountException(final String message, final Throwable why) {
|
||||
super(message, why);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
// 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 com.google.gerrit.client.openid.OpenIdUtil;
|
||||
import com.google.gerrit.client.reviewdb.Account;
|
||||
import com.google.gerrit.client.reviewdb.AccountExternalId;
|
||||
import com.google.gerrit.client.reviewdb.ReviewDb;
|
||||
import com.google.gerrit.server.config.AuthConfig;
|
||||
import com.google.gwtorm.client.OrmException;
|
||||
import com.google.gwtorm.client.SchemaFactory;
|
||||
import com.google.gwtorm.client.Transaction;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/** Tracks authentication related details for user accounts. */
|
||||
@Singleton
|
||||
public class AccountManager {
|
||||
private final SchemaFactory<ReviewDb> schema;
|
||||
private final AccountCache byIdCache;
|
||||
private final AccountByEmailCache byEmailCache;
|
||||
private final EmailExpander emailExpander;
|
||||
private final AuthConfig authConfig;
|
||||
|
||||
@Inject
|
||||
AccountManager(final SchemaFactory<ReviewDb> schema,
|
||||
final AccountCache byIdCache, final AccountByEmailCache byEmailCache,
|
||||
final EmailExpander emailExpander, final AuthConfig authConfig) {
|
||||
this.schema = schema;
|
||||
this.byIdCache = byIdCache;
|
||||
this.byEmailCache = byEmailCache;
|
||||
this.emailExpander = emailExpander;
|
||||
this.authConfig = authConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if user identified by this external identity string has an account.
|
||||
*/
|
||||
public boolean exists(final String externalId) throws AccountException {
|
||||
try {
|
||||
final ReviewDb db = schema.open();
|
||||
try {
|
||||
final List<AccountExternalId> matches =
|
||||
db.accountExternalIds().byExternal(externalId).toList();
|
||||
return !matches.isEmpty();
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
} catch (OrmException e) {
|
||||
throw new AccountException("Cannot lookup account " + externalId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate the user, potentially creating a new account if they are new.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
public AuthResult authenticate(final AuthRequest who) throws AccountException {
|
||||
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);
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
} catch (OrmException e) {
|
||||
throw new AccountException("Authentication error", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void update(final ReviewDb db, final AuthRequest who,
|
||||
final AccountExternalId extId) throws OrmException, AccountException {
|
||||
final Transaction txn = db.beginTransaction();
|
||||
final Account account = db.accounts().get(extId.getAccountId());
|
||||
if (account == null) {
|
||||
throw new AccountException("Account has been deleted");
|
||||
}
|
||||
|
||||
// If the email address was modified by the authentication provider,
|
||||
// update our records to match the changed email.
|
||||
//
|
||||
final String newEmail = who.getEmailAddress();
|
||||
final String oldEmail = extId.getEmailAddress();
|
||||
if (newEmail != null && !newEmail.equals(oldEmail)) {
|
||||
if (oldEmail != null && oldEmail.equals(account.getPreferredEmail())) {
|
||||
account.setPreferredEmail(newEmail);
|
||||
db.accounts().update(Collections.singleton(account), txn);
|
||||
}
|
||||
|
||||
extId.setEmailAddress(newEmail);
|
||||
}
|
||||
|
||||
extId.setLastUsedOn();
|
||||
db.accountExternalIds().update(Collections.singleton(extId), txn);
|
||||
txn.commit();
|
||||
|
||||
if (newEmail != null && !newEmail.equals(oldEmail)) {
|
||||
byEmailCache.evict(oldEmail);
|
||||
byEmailCache.evict(newEmail);
|
||||
}
|
||||
}
|
||||
|
||||
private AuthResult create(final ReviewDb db, final AuthRequest who)
|
||||
throws OrmException, AccountException {
|
||||
if (authConfig.isAllowGoogleAccountUpgrade()
|
||||
&& who.getExternalId().startsWith(OpenIdUtil.URL_GOOGLE + "?")
|
||||
&& who.getEmailAddress() != null) {
|
||||
final List<AccountExternalId> legacyAppEngine =
|
||||
db.accountExternalIds().byExternal(
|
||||
AccountExternalId.LEGACY_GAE + who.getEmailAddress()).toList();
|
||||
|
||||
if (legacyAppEngine.size() == 1) {
|
||||
// Exactly one user was imported from Gerrit 1.x with this email
|
||||
// address. Upgrade their account by deleting the legacy import
|
||||
// identity and creating a new identity matching the token we have.
|
||||
//
|
||||
final AccountExternalId oldId = legacyAppEngine.get(0);
|
||||
final AccountExternalId newId = createId(oldId.getAccountId(), who);
|
||||
newId.setEmailAddress(who.getEmailAddress());
|
||||
newId.setLastUsedOn();
|
||||
final Transaction txn = db.beginTransaction();
|
||||
db.accountExternalIds().delete(Collections.singleton(oldId), txn);
|
||||
db.accountExternalIds().insert(Collections.singleton(newId), txn);
|
||||
txn.commit();
|
||||
return new AuthResult(newId.getAccountId(), false);
|
||||
|
||||
} else if (legacyAppEngine.size() > 1) {
|
||||
throw new AccountException("Multiple Gerrit 1.x accounts found");
|
||||
}
|
||||
}
|
||||
|
||||
final Account.Id newId = new Account.Id(db.nextAccountId());
|
||||
final Account account = new Account(newId);
|
||||
final AccountExternalId extId = createId(newId, who);
|
||||
|
||||
if (who.getLocalUser() != null && who.getEmailAddress() == null) {
|
||||
// A SCHEMA_GERRIT account was authenticated by an external SSO
|
||||
// solution. The external identity string actually contains a
|
||||
// name that we can uniquely refer to the user by, so set
|
||||
// account information based upon that name.
|
||||
//
|
||||
final String user = who.getLocalUser();
|
||||
if (emailExpander.canExpand(user)) {
|
||||
extId.setEmailAddress(emailExpander.expand(user));
|
||||
}
|
||||
account.setSshUserName(user);
|
||||
} else if (who.getEmailAddress() != null) {
|
||||
extId.setEmailAddress(who.getEmailAddress());
|
||||
}
|
||||
|
||||
extId.setLastUsedOn();
|
||||
account.setFullName(who.getDisplayName());
|
||||
account.setPreferredEmail(extId.getEmailAddress());
|
||||
|
||||
final Transaction txn = db.beginTransaction();
|
||||
db.accounts().insert(Collections.singleton(account), txn);
|
||||
db.accountExternalIds().insert(Collections.singleton(extId), txn);
|
||||
txn.commit();
|
||||
|
||||
byEmailCache.evict(account.getPreferredEmail());
|
||||
return new AuthResult(newId, true);
|
||||
}
|
||||
|
||||
private static AccountExternalId createId(final Account.Id newId,
|
||||
final AuthRequest who) {
|
||||
final String ext = who.getExternalId();
|
||||
return new AccountExternalId(new AccountExternalId.Key(newId, ext));
|
||||
}
|
||||
|
||||
/**
|
||||
* Link another authentication identity to an existing account.
|
||||
*
|
||||
* @param to account to link the identity onto.
|
||||
* @param who the additional identity.
|
||||
* @throws AccountException the identity belongs to a different account, or it
|
||||
* cannot be linked at this time.
|
||||
*/
|
||||
public void link(final Account.Id to, final AuthRequest who)
|
||||
throws AccountException {
|
||||
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;
|
||||
}
|
||||
|
||||
case 1: {
|
||||
final AccountExternalId extId = matches.get(0);
|
||||
if (!extId.getAccountId().equals(to)) {
|
||||
throw new AccountException("Identity already used");
|
||||
}
|
||||
update(db, who, extId);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new AccountException("Identity already used");
|
||||
}
|
||||
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
} catch (OrmException e) {
|
||||
throw new AccountException("Cannot link identity", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// 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.client.reviewdb.AccountExternalId.SCHEME_GERRIT;
|
||||
import static com.google.gerrit.client.reviewdb.AccountExternalId.SCHEME_MAILTO;
|
||||
|
||||
/**
|
||||
* Information for {@link AccountManager#authenticate(AuthRequest)}.
|
||||
* <p>
|
||||
* Callers should populate this object with as much information as possible
|
||||
* about the user account. For example, OpenID authentication might return
|
||||
* registration information including a display name for the user, and an email
|
||||
* address for them. These fields however are optional, as not all OpenID
|
||||
* providers return them, and not all non-OpenID systems can use them.
|
||||
*/
|
||||
public class AuthRequest {
|
||||
/** Create a request for a local username, such as from LDAP. */
|
||||
public static AuthRequest forUser(final String username) {
|
||||
return new AuthRequest(SCHEME_GERRIT + username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a request for an email address registration.
|
||||
* <p>
|
||||
* This type of request should be used only to attach a new email address to
|
||||
* an existing user account.
|
||||
*/
|
||||
public static AuthRequest forEmail(final String email) {
|
||||
final AuthRequest r;
|
||||
r = new AuthRequest(SCHEME_MAILTO + email);
|
||||
r.setEmailAddress(email);
|
||||
return r;
|
||||
}
|
||||
|
||||
private final String externalId;
|
||||
private String displayName;
|
||||
private String emailAddress;
|
||||
|
||||
public AuthRequest(final String externalId) {
|
||||
this.externalId = externalId;
|
||||
}
|
||||
|
||||
public String getExternalId() {
|
||||
return externalId;
|
||||
}
|
||||
|
||||
public String getLocalUser() {
|
||||
if (getExternalId().startsWith(SCHEME_GERRIT)) {
|
||||
return getExternalId().substring(SCHEME_GERRIT.length());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public void setDisplayName(final String name) {
|
||||
displayName = name != null && name.length() > 0 ? name : null;
|
||||
}
|
||||
|
||||
public String getEmailAddress() {
|
||||
return emailAddress;
|
||||
}
|
||||
|
||||
public void setEmailAddress(final String email) {
|
||||
emailAddress = email != null && email.length() > 0 ? email : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// 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 com.google.gerrit.client.reviewdb.Account;
|
||||
|
||||
/** Result from {@link AccountManager#authenticate(AuthRequest)}. */
|
||||
public class AuthResult {
|
||||
private final Account.Id accountId;
|
||||
private final boolean isNew;
|
||||
|
||||
AuthResult(final Account.Id accountId, final boolean isNew) {
|
||||
this.accountId = accountId;
|
||||
this.isNew = isNew;
|
||||
}
|
||||
|
||||
/** Identity of the user account that was authenticated into. */
|
||||
public Account.Id getAccountId() {
|
||||
return accountId;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if this account was recently created for the user.
|
||||
* <p>
|
||||
* New users should be redirected to the registration screen, so they can
|
||||
* configure their new user account.
|
||||
*/
|
||||
public boolean isNew() {
|
||||
return isNew;
|
||||
}
|
||||
}
|
||||
@@ -67,22 +67,12 @@ public class CanonicalWebUrlProvider implements Provider<String> {
|
||||
}
|
||||
}
|
||||
|
||||
// Assume this servlet is in the context with a simple name like "login"
|
||||
// and we were accessed without any path info. Clipping the last part of
|
||||
// the name from the URL should generate the web application's root path.
|
||||
//
|
||||
String uri = req.getRequestURL().toString();
|
||||
final int s = uri.lastIndexOf('/');
|
||||
if (s >= 0) {
|
||||
uri = uri.substring(0, s + 1);
|
||||
final StringBuffer url = req.getRequestURL();
|
||||
url.setLength(url.length() - req.getServletPath().length());
|
||||
if (url.charAt(url.length() - 1) != '/') {
|
||||
url.append('/');
|
||||
}
|
||||
final String sfx = "/gerrit/rpc/";
|
||||
if (uri.endsWith(sfx)) {
|
||||
// Nope, it was one of our RPC servlets. Drop the rpc too.
|
||||
//
|
||||
uri = uri.substring(0, uri.length() - (sfx.length() - 1));
|
||||
}
|
||||
return uri;
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
// We have no way of guessing our HTTP url.
|
||||
|
||||
@@ -16,48 +16,35 @@ package com.google.gerrit.server.http;
|
||||
|
||||
import com.google.gerrit.client.Gerrit;
|
||||
import com.google.gerrit.client.reviewdb.Account;
|
||||
import com.google.gerrit.client.reviewdb.AccountExternalId;
|
||||
import com.google.gerrit.client.reviewdb.ReviewDb;
|
||||
import com.google.gerrit.server.account.AccountByEmailCache;
|
||||
import com.google.gerrit.server.account.EmailExpander;
|
||||
import com.google.gerrit.server.config.AuthConfig;
|
||||
import com.google.gwtjsonrpc.server.ActiveCall;
|
||||
import com.google.gwtjsonrpc.server.SignedToken;
|
||||
import com.google.gwtjsonrpc.server.ValidToken;
|
||||
import com.google.gwtjsonrpc.server.XsrfException;
|
||||
import com.google.gwtorm.client.OrmException;
|
||||
import com.google.gwtorm.client.SchemaFactory;
|
||||
import com.google.gwtorm.client.Transaction;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.servlet.RequestScoped;
|
||||
|
||||
import org.spearce.jgit.util.Base64;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
@RequestScoped
|
||||
public class GerritCall extends ActiveCall {
|
||||
private final AuthConfig authConfig;
|
||||
private final EmailExpander emailExpander;
|
||||
private final SchemaFactory<ReviewDb> schema;
|
||||
private final AccountByEmailCache byEmailCache;
|
||||
private final SignedToken sessionKey;
|
||||
private final int sessionAge;
|
||||
|
||||
private boolean accountRead;
|
||||
private Account.Id accountId;
|
||||
private boolean rememberAccount;
|
||||
|
||||
@Inject
|
||||
GerritCall(final AuthConfig ac, final EmailExpander emailExpander,
|
||||
final SchemaFactory<ReviewDb> sf, final AccountByEmailCache bec,
|
||||
final HttpServletRequest i, final HttpServletResponse o) {
|
||||
GerritCall(final AuthConfig ac, final HttpServletRequest i,
|
||||
final HttpServletResponse o) {
|
||||
super(i, o);
|
||||
this.authConfig = ac;
|
||||
this.emailExpander = emailExpander;
|
||||
this.schema = sf;
|
||||
this.byEmailCache = bec;
|
||||
setXsrfSignedToken(ac.getXsrfToken());
|
||||
sessionKey = ac.getAccountToken();
|
||||
sessionAge = ac.getSessionAge();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -95,139 +82,31 @@ public class GerritCall extends ActiveCall {
|
||||
}
|
||||
|
||||
private void initAccount() {
|
||||
if (accountRead) {
|
||||
return;
|
||||
}
|
||||
if (!accountRead) {
|
||||
accountRead = true;
|
||||
accountId = null;
|
||||
rememberAccount = false;
|
||||
|
||||
accountRead = true;
|
||||
ValidToken accountInfo =
|
||||
getCookie(Gerrit.ACCOUNT_COOKIE, authConfig.getAccountToken());
|
||||
|
||||
if (accountInfo == null) {
|
||||
switch (authConfig.getLoginType()) {
|
||||
case HTTP:
|
||||
if (assumeHttp()) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (getCookie(Gerrit.ACCOUNT_COOKIE) != null) {
|
||||
// The cookie is bogus, but it was sent. Send an expired cookie
|
||||
// back to clear it out of the browser's cookie store.
|
||||
//
|
||||
logout();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final AccountCookie cookie = AccountCookie.parse(accountInfo);
|
||||
accountId = cookie.getAccountId();
|
||||
rememberAccount = cookie.isRemember();
|
||||
} catch (RuntimeException e) {
|
||||
// Whoa, did we change our cookie format or something? This should
|
||||
// never happen on a valid acocunt token, but discard it anyway.
|
||||
//
|
||||
logout();
|
||||
return;
|
||||
}
|
||||
|
||||
if (accountInfo.needsRefresh()) {
|
||||
// The cookie is valid, but its getting stale. Update it with a
|
||||
// newer date so it doesn't expire on an active user.
|
||||
//
|
||||
setAccountCookie();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean assumeHttp() {
|
||||
final String hdr = authConfig.getLoginHttpHeader();
|
||||
String user;
|
||||
if (hdr != null && !"".equals(hdr)
|
||||
&& !"Authorization".equalsIgnoreCase(hdr)) {
|
||||
user = getHttpServletRequest().getHeader(hdr);
|
||||
} else {
|
||||
user = getHttpServletRequest().getRemoteUser();
|
||||
if (user == null) {
|
||||
// If the container didn't do the authentication we might
|
||||
// have done it in the front-end web server. Try to split
|
||||
// the identity out of the Authorization header and honor it.
|
||||
//
|
||||
user = getHttpServletRequest().getHeader("Authorization");
|
||||
if (user != null && user.startsWith("Basic ")) {
|
||||
user = new String(Base64.decode(user.substring("Basic ".length())));
|
||||
if (user.indexOf(':') >= 0) {
|
||||
user = user.substring(0, user.indexOf(':'));
|
||||
}
|
||||
} else if (user != null && user.startsWith("Digest ")
|
||||
&& user.contains("username=\"")) {
|
||||
user = user.substring(user.indexOf("username=\"") + 10);
|
||||
user = user.substring(0, user.indexOf('"'));
|
||||
} else {
|
||||
user = null;
|
||||
final ValidToken t = getCookie(Gerrit.ACCOUNT_COOKIE, sessionKey);
|
||||
if (t != null) {
|
||||
final AccountCookie cookie;
|
||||
try {
|
||||
cookie = AccountCookie.parse(t);
|
||||
} catch (RuntimeException e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (user == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
final ReviewDb db = schema.open();
|
||||
try {
|
||||
final String eid = AccountExternalId.SCHEME_GERRIT + user;
|
||||
final List<AccountExternalId> matches =
|
||||
db.accountExternalIds().byExternal(eid).toList();
|
||||
accountId = cookie.getAccountId();
|
||||
rememberAccount = cookie.isRemember();
|
||||
|
||||
if (matches.size() == 1) {
|
||||
// Account exists, connect to it again.
|
||||
if (t.needsRefresh()) {
|
||||
// The cookie is valid, but its getting stale. Update it with a
|
||||
// newer date so it doesn't expire on an active user.
|
||||
//
|
||||
final AccountExternalId e = matches.get(0);
|
||||
e.setLastUsedOn();
|
||||
db.accountExternalIds().update(Collections.singleton(e));
|
||||
|
||||
accountId = e.getAccountId();
|
||||
rememberAccount = false;
|
||||
setAccountCookie();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (matches.size() == 0) {
|
||||
// No account, automatically initialize a new one.
|
||||
//
|
||||
final Transaction txn = db.beginTransaction();
|
||||
final Account.Id nid = new Account.Id(db.nextAccountId());
|
||||
final Account a = new Account(nid);
|
||||
a.setFullName(user);
|
||||
if (emailExpander.canExpand(user)) {
|
||||
a.setPreferredEmail(emailExpander.expand(user));
|
||||
final int at = a.getPreferredEmail().indexOf('@');
|
||||
if (at > 0) {
|
||||
a.setSshUserName(a.getPreferredEmail().substring(0, at));
|
||||
}
|
||||
}
|
||||
|
||||
final AccountExternalId e =
|
||||
new AccountExternalId(new AccountExternalId.Key(nid, eid));
|
||||
e.setEmailAddress(a.getPreferredEmail());
|
||||
e.setLastUsedOn();
|
||||
db.accounts().insert(Collections.singleton(a), txn);
|
||||
db.accountExternalIds().insert(Collections.singleton(e), txn);
|
||||
txn.commit();
|
||||
byEmailCache.evict(a.getPreferredEmail());
|
||||
|
||||
accountId = nid;
|
||||
rememberAccount = false;
|
||||
setAccountCookie();
|
||||
return true;
|
||||
}
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
} catch (OrmException e) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void setAccountCookie() {
|
||||
@@ -235,12 +114,19 @@ public class GerritCall extends ActiveCall {
|
||||
String val;
|
||||
int age;
|
||||
try {
|
||||
val = authConfig.getAccountToken().newToken(ac.toString());
|
||||
age = ac.isRemember() ? authConfig.getSessionAge() : -1;
|
||||
val = sessionKey.newToken(ac.toString());
|
||||
age = ac.isRemember() ? sessionAge : -1;
|
||||
} catch (XsrfException e) {
|
||||
val = "";
|
||||
age = 0;
|
||||
}
|
||||
setCookie(Gerrit.ACCOUNT_COOKIE, val, age);
|
||||
String path = getHttpServletRequest().getContextPath();
|
||||
if (path.equals("")) {
|
||||
path = "/";
|
||||
}
|
||||
final Cookie c = new Cookie(Gerrit.ACCOUNT_COOKIE, val);
|
||||
c.setMaxAge(age);
|
||||
c.setPath(path);
|
||||
httpResponse.addCookie(c);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,13 +114,22 @@ public class GerritServletConfig extends GuiceServletContextListener {
|
||||
final AuthConfig auth = sysInjector.getInstance(AuthConfig.class);
|
||||
|
||||
final List<Module> modules = new ArrayList<Module>();
|
||||
modules.add(new WebModule(sshInfo));
|
||||
|
||||
modules.add(new ServletModule() {
|
||||
@Override
|
||||
protected void configureServlets() {
|
||||
filter("/*").through(RequestCleanupFilter.class);
|
||||
filter("/*").through(UrlRewriteFilter.class);
|
||||
}
|
||||
});
|
||||
switch (auth.getLoginType()) {
|
||||
case OPENID:
|
||||
modules.add(new OpenIdModule());
|
||||
break;
|
||||
|
||||
case HTTP:
|
||||
modules.add(new HttpAuthModule());
|
||||
break;
|
||||
|
||||
case DEVELOPMENT_BECOME_ANY_ACCOUNT:
|
||||
modules.add(new ServletModule() {
|
||||
@Override
|
||||
@@ -130,6 +139,7 @@ public class GerritServletConfig extends GuiceServletContextListener {
|
||||
});
|
||||
break;
|
||||
}
|
||||
modules.add(new WebModule(sshInfo));
|
||||
|
||||
return sysInjector.createChildInjector(modules);
|
||||
}
|
||||
|
||||
113
src/main/java/com/google/gerrit/server/http/HttpAuthFilter.java
Normal file
113
src/main/java/com/google/gerrit/server/http/HttpAuthFilter.java
Normal file
@@ -0,0 +1,113 @@
|
||||
// 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.http;
|
||||
|
||||
import com.google.gwt.user.server.rpc.RPCServletUtils;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.FilterConfig;
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* Watches request for the host page and requires login if not yet signed in.
|
||||
* <p>
|
||||
* If HTTP authentication has been enabled on this server this filter is bound
|
||||
* in front of the {@link HostPageServlet} and redirects users who are not yet
|
||||
* signed in to visit {@code /login/}, so the web container can force login.
|
||||
* This redirect is performed with JavaScript, such that any existing anchor
|
||||
* token in the URL can be rewritten and preserved through the authentication
|
||||
* process of any enterprise single sign-on solutions.
|
||||
*/
|
||||
@Singleton
|
||||
class HttpAuthFilter implements Filter {
|
||||
private final Provider<GerritCall> gerritCall;
|
||||
private final byte[] signInRaw;
|
||||
private final byte[] signInGzip;
|
||||
|
||||
@Inject
|
||||
HttpAuthFilter(final Provider<GerritCall> gerritCall,
|
||||
final ServletContext servletContext) throws IOException {
|
||||
this.gerritCall = gerritCall;
|
||||
|
||||
final String hostPageName = "WEB-INF/LoginRedirect.html";
|
||||
final String doc = HtmlDomUtil.readFile(servletContext, "/" + hostPageName);
|
||||
if (doc == null) {
|
||||
throw new FileNotFoundException("No " + hostPageName + " in webapp");
|
||||
}
|
||||
|
||||
signInRaw = doc.getBytes(HtmlDomUtil.ENC);
|
||||
signInGzip = HtmlDomUtil.compress(signInRaw);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(final ServletRequest request,
|
||||
final ServletResponse response, final FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
if (gerritCall.get().getAccountId() == null) {
|
||||
// Not signed in yet. Since the browser state might have an anchor
|
||||
// token which we want to capture and carry through the auth process
|
||||
// we send back JavaScript now to capture that, and do the real work
|
||||
// of redirecting to the authentication area.
|
||||
//
|
||||
final HttpServletRequest req = (HttpServletRequest) request;
|
||||
final HttpServletResponse rsp = (HttpServletResponse) response;
|
||||
final byte[] tosend;
|
||||
if (RPCServletUtils.acceptsGzipEncoding(req)) {
|
||||
rsp.setHeader("Content-Encoding", "gzip");
|
||||
tosend = signInGzip;
|
||||
} else {
|
||||
tosend = signInRaw;
|
||||
}
|
||||
|
||||
rsp.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
|
||||
rsp.setHeader("Pragma", "no-cache");
|
||||
rsp.setHeader("Cache-Control", "no-cache, must-revalidate");
|
||||
rsp.setContentType("text/html");
|
||||
rsp.setCharacterEncoding(HtmlDomUtil.ENC);
|
||||
rsp.setContentLength(tosend.length);
|
||||
final OutputStream out = rsp.getOutputStream();
|
||||
try {
|
||||
out.write(tosend);
|
||||
} finally {
|
||||
out.close();
|
||||
}
|
||||
} else {
|
||||
// Already signed in, forward the request.
|
||||
//
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(final FilterConfig filterConfig) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// 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.http;
|
||||
|
||||
import com.google.inject.servlet.ServletModule;
|
||||
|
||||
/** Servlets and support related to HTTP authentication. */
|
||||
class HttpAuthModule extends ServletModule {
|
||||
@Override
|
||||
protected void configureServlets() {
|
||||
filter("/").through(HttpAuthFilter.class);
|
||||
serve("/login/*").with(HttpLoginServlet.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// 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.http;
|
||||
|
||||
import com.google.gerrit.client.Link;
|
||||
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.AuthResult;
|
||||
import com.google.gerrit.server.config.AuthConfig;
|
||||
import com.google.gerrit.server.config.CanonicalWebUrl;
|
||||
import com.google.gerrit.server.config.Nullable;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.spearce.jgit.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* Initializes the user session if HTTP authentication is enabled.
|
||||
* <p>
|
||||
* If HTTP authentication has been enabled this servlet binds to {@code /login/}
|
||||
* and initializes the user session based on user information contained in the
|
||||
* HTTP request.
|
||||
*/
|
||||
@Singleton
|
||||
class HttpLoginServlet extends HttpServlet {
|
||||
private static final Logger log =
|
||||
LoggerFactory.getLogger(HttpLoginServlet.class);
|
||||
|
||||
private static final String AUTHORIZATION = "Authorization";
|
||||
private final Provider<GerritCall> gerritCall;
|
||||
private final Provider<String> urlProvider;
|
||||
private final AccountManager accountManager;
|
||||
private final String loginHeader;
|
||||
|
||||
@Inject
|
||||
HttpLoginServlet(final AuthConfig authConfig,
|
||||
final Provider<GerritCall> gerritCall,
|
||||
@CanonicalWebUrl @Nullable final Provider<String> urlProvider,
|
||||
final AccountManager accountManager) {
|
||||
this.gerritCall = gerritCall;
|
||||
this.urlProvider = urlProvider;
|
||||
this.accountManager = accountManager;
|
||||
|
||||
final String hdr = authConfig.getLoginHttpHeader();
|
||||
this.loginHeader = hdr != null && !hdr.equals("") ? hdr : AUTHORIZATION;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doGet(final HttpServletRequest req,
|
||||
final HttpServletResponse rsp) throws IOException {
|
||||
final String user = getRemoteUser(req);
|
||||
if (user == null || "".equals(user)) {
|
||||
log.error("Unable to authenticate user by " + loginHeader
|
||||
+ " request header. Check container or server configuration.");
|
||||
rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
|
||||
return;
|
||||
}
|
||||
|
||||
final AuthRequest areq = AuthRequest.forUser(user);
|
||||
final AuthResult arsp;
|
||||
try {
|
||||
arsp = accountManager.authenticate(areq);
|
||||
} catch (AccountException e) {
|
||||
log.error("Unable to authenticate user \"" + user + "\"", e);
|
||||
rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
|
||||
return;
|
||||
}
|
||||
|
||||
String token = req.getPathInfo();
|
||||
if (token != null && token.startsWith("/")) {
|
||||
token = token.substring(1);
|
||||
}
|
||||
if (token == null || token.isEmpty()) {
|
||||
token = Link.MINE;
|
||||
}
|
||||
|
||||
gerritCall.get().setAccount(arsp.getAccountId(), false);
|
||||
final StringBuilder rdr = new StringBuilder();
|
||||
rdr.append(urlProvider.get());
|
||||
rdr.append('#');
|
||||
if (arsp.isNew()) {
|
||||
rdr.append(Link.REGISTER);
|
||||
rdr.append(',');
|
||||
}
|
||||
rdr.append(token);
|
||||
rsp.sendRedirect(rdr.toString());
|
||||
}
|
||||
|
||||
private String getRemoteUser(final HttpServletRequest req) {
|
||||
if (AUTHORIZATION.equals(loginHeader)) {
|
||||
final String user = req.getRemoteUser();
|
||||
if (user != null && !"".equals(user)) {
|
||||
// The container performed the authentication, and has the user
|
||||
// identity already decoded for us. Honor that as we have been
|
||||
// configured to honor HTTP authentication.
|
||||
//
|
||||
return user;
|
||||
}
|
||||
|
||||
// If the container didn't do the authentication we might
|
||||
// have done it in the front-end web server. Try to split
|
||||
// the identity out of the Authorization header and honor it.
|
||||
//
|
||||
String auth = req.getHeader(AUTHORIZATION);
|
||||
if (auth == null || "".equals(auth)) {
|
||||
return null;
|
||||
|
||||
} else if (auth.startsWith("Basic ")) {
|
||||
auth = auth.substring("Basic ".length());
|
||||
auth = new String(Base64.decode(auth));
|
||||
final int c = auth.indexOf(':');
|
||||
return c > 0 ? auth.substring(0, c) : null;
|
||||
|
||||
} else if (auth.startsWith("Digest ")) {
|
||||
final int u = auth.indexOf("username=\"");
|
||||
if (u <= 0) {
|
||||
return null;
|
||||
}
|
||||
auth = auth.substring(u + 10);
|
||||
final int e = auth.indexOf('"');
|
||||
return e > 0 ? auth.substring(0, auth.indexOf('"')) : null;
|
||||
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
// Nonstandard HTTP header. We have been told to trust this
|
||||
// header blindly as-is.
|
||||
//
|
||||
final String user = req.getHeader(loginHeader);
|
||||
return user != null && !"".equals(user) ? user : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,13 +20,13 @@ import com.google.gerrit.client.data.GerritConfig;
|
||||
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.config.FactoryModule;
|
||||
import com.google.gerrit.server.config.GerritConfigProvider;
|
||||
import com.google.gerrit.server.config.GerritRequestModule;
|
||||
import com.google.gerrit.server.rpc.UiRpcModule;
|
||||
import com.google.gerrit.server.ssh.SshInfo;
|
||||
import com.google.gwtexpui.server.CacheControlFilter;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Key;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.servlet.RequestScoped;
|
||||
@@ -37,9 +37,8 @@ import java.net.SocketAddress;
|
||||
class WebModule extends FactoryModule {
|
||||
private final Provider<SshInfo> sshInfoProvider;
|
||||
|
||||
@Inject
|
||||
WebModule(final Provider<SshInfo> si) {
|
||||
sshInfoProvider = si;
|
||||
WebModule(final Provider<SshInfo> sshInfoProvider) {
|
||||
this.sshInfoProvider = sshInfoProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -47,9 +46,6 @@ class WebModule extends FactoryModule {
|
||||
install(new ServletModule() {
|
||||
@Override
|
||||
protected void configureServlets() {
|
||||
filter("/*").through(RequestCleanupFilter.class);
|
||||
filter("/*").through(UrlRewriteFilter.class);
|
||||
|
||||
filter("/*").through(Key.get(CacheControlFilter.class));
|
||||
bind(Key.get(CacheControlFilter.class)).in(SINGLETON);
|
||||
|
||||
@@ -67,6 +63,7 @@ class WebModule extends FactoryModule {
|
||||
bind(SshInfo.class).toProvider(sshInfoProvider);
|
||||
bind(GerritConfig.class).toProvider(GerritConfigProvider.class).in(
|
||||
SINGLETON);
|
||||
bind(AccountManager.class).in(SINGLETON);
|
||||
bind(GerritCall.class).in(RequestScoped.class);
|
||||
bind(SocketAddress.class).annotatedWith(RemotePeer.class).toProvider(
|
||||
HttpRemotePeerProvider.class).in(RequestScoped.class);
|
||||
|
||||
@@ -23,14 +23,14 @@ import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/** Handles the <code>/login</code> URL for web based single-sign-on. */
|
||||
/** Handles the <code>/OpenID</code> URL for web based single-sign-on. */
|
||||
@SuppressWarnings("serial")
|
||||
@Singleton
|
||||
class OpenIdLoginServlet extends HttpServlet {
|
||||
private final OpenIdServiceImpl impl;
|
||||
|
||||
@Inject
|
||||
OpenIdLoginServlet(final OpenIdServiceImpl i){
|
||||
OpenIdLoginServlet(final OpenIdServiceImpl i) {
|
||||
impl = i;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class OpenIdLoginServlet extends HttpServlet {
|
||||
public void doPost(final HttpServletRequest req, final HttpServletResponse rsp)
|
||||
throws IOException {
|
||||
try {
|
||||
rsp.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
|
||||
rsp.setHeader("Pragma", "no-cache");
|
||||
rsp.setHeader("Cache-Control", "no-cache, must-revalidate");
|
||||
impl.doAuth(req, rsp);
|
||||
} catch (Exception e) {
|
||||
getServletContext().log("Unexpected error during authentication", e);
|
||||
|
||||
@@ -20,24 +20,15 @@ import com.google.gerrit.client.SignInDialog.Mode;
|
||||
import com.google.gerrit.client.openid.DiscoveryResult;
|
||||
import com.google.gerrit.client.openid.OpenIdService;
|
||||
import com.google.gerrit.client.openid.OpenIdUtil;
|
||||
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.gerrit.server.IdentifiedUser;
|
||||
import com.google.gerrit.server.UrlEncoded;
|
||||
import com.google.gerrit.server.account.AccountByEmailCache;
|
||||
import com.google.gerrit.server.account.AccountCache;
|
||||
import com.google.gerrit.server.config.AuthConfig;
|
||||
import com.google.gerrit.server.account.AccountException;
|
||||
import com.google.gerrit.server.account.AccountManager;
|
||||
import com.google.gerrit.server.config.CanonicalWebUrl;
|
||||
import com.google.gerrit.server.config.Nullable;
|
||||
import com.google.gerrit.server.http.GerritCall;
|
||||
import com.google.gwt.user.client.rpc.AsyncCallback;
|
||||
import com.google.gwtorm.client.KeyUtil;
|
||||
import com.google.gwtorm.client.OrmException;
|
||||
import com.google.gwtorm.client.ResultSet;
|
||||
import com.google.gwtorm.client.SchemaFactory;
|
||||
import com.google.gwtorm.client.Transaction;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.Singleton;
|
||||
@@ -69,10 +60,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@@ -102,28 +90,21 @@ class OpenIdServiceImpl implements OpenIdService {
|
||||
|
||||
private final Provider<GerritCall> callFactory;
|
||||
private final Provider<IdentifiedUser> identifiedUser;
|
||||
private final AuthConfig authConfig;
|
||||
private final Provider<String> urlProvider;
|
||||
private final SchemaFactory<ReviewDb> schema;
|
||||
private final AccountManager accountManager;
|
||||
private final ConsumerManager manager;
|
||||
private final AccountByEmailCache byEmailCache;
|
||||
private final AccountCache byIdCache;
|
||||
private final SelfPopulatingCache discoveryCache;
|
||||
|
||||
@Inject
|
||||
OpenIdServiceImpl(final Provider<GerritCall> cf,
|
||||
final Provider<IdentifiedUser> iu, final AuthConfig ac,
|
||||
final Provider<IdentifiedUser> iu,
|
||||
@CanonicalWebUrl @Nullable final Provider<String> up,
|
||||
final AccountByEmailCache bec, final AccountCache bic,
|
||||
final CacheManager cacheMgr, final SchemaFactory<ReviewDb> sf)
|
||||
final CacheManager cacheMgr, final AccountManager am)
|
||||
throws ConsumerException {
|
||||
callFactory = cf;
|
||||
identifiedUser = iu;
|
||||
authConfig = ac;
|
||||
urlProvider = up;
|
||||
schema = sf;
|
||||
byEmailCache = bec;
|
||||
byIdCache = bic;
|
||||
accountManager = am;
|
||||
manager = new ConsumerManager();
|
||||
|
||||
final Cache base = cacheMgr.getCache("openid");
|
||||
@@ -187,217 +168,146 @@ class OpenIdServiceImpl implements OpenIdService {
|
||||
// registration information, in case the identity is new to us.
|
||||
//
|
||||
return true;
|
||||
}
|
||||
|
||||
// We might already have this account on file. Look for it.
|
||||
//
|
||||
try {
|
||||
final ReviewDb db = schema.open();
|
||||
try {
|
||||
final ResultSet<AccountExternalId> ae =
|
||||
db.accountExternalIds().byExternal(aReq.getIdentity());
|
||||
if (ae.iterator().hasNext()) {
|
||||
// We already have it. Don't bother asking for the
|
||||
// registration information, we have what we need.
|
||||
//
|
||||
return false;
|
||||
}
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
} catch (OrmException e) {
|
||||
log.warn("Failed looking for existing account", e);
|
||||
} else {
|
||||
// We might already have this account on file. Look for it.
|
||||
//
|
||||
return accountManager.equals(aReq.getIdentity());
|
||||
}
|
||||
|
||||
// We don't have this account on file, or our query failed. Assume
|
||||
// we should ask for registration information in case the account
|
||||
// turns out to be new.
|
||||
//
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Called by {@link OpenIdLoginServlet} doGet, doPost */
|
||||
void doAuth(final HttpServletRequest req, final HttpServletResponse rsp)
|
||||
throws Exception {
|
||||
if (false) {
|
||||
debugRequest(req);
|
||||
if (OMODE_CANCEL.equals(req.getParameter(OPENID_MODE))) {
|
||||
cancel(req, rsp);
|
||||
return;
|
||||
}
|
||||
|
||||
callFactory.get().noCache();
|
||||
// Process the authentication response.
|
||||
//
|
||||
final SignInDialog.Mode mode = signInMode(req);
|
||||
final String openidIdentifier = req.getParameter("openid.identity");
|
||||
final String returnToken = req.getParameter(P_TOKEN);
|
||||
final boolean remember = "1".equals(req.getParameter(P_REMEMBER));
|
||||
final State state;
|
||||
|
||||
final String openidMode = req.getParameter(OPENID_MODE);
|
||||
if (OMODE_CANCEL.equals(openidMode)) {
|
||||
cancel(req, rsp);
|
||||
|
||||
} else {
|
||||
// Process the authentication response.
|
||||
state = init(openidIdentifier, mode, remember, returnToken);
|
||||
if (state == null) {
|
||||
// Re-discovery must have failed, we can't run a login.
|
||||
//
|
||||
final SignInDialog.Mode mode = signInMode(req);
|
||||
final String openidIdentifier = req.getParameter("openid.identity");
|
||||
final String returnToken = req.getParameter(P_TOKEN);
|
||||
final boolean remember = "1".equals(req.getParameter(P_REMEMBER));
|
||||
final State state;
|
||||
cancel(req, rsp);
|
||||
return;
|
||||
}
|
||||
|
||||
state = init(openidIdentifier, mode, remember, returnToken);
|
||||
if (state == null) {
|
||||
// Re-discovery must have failed, we can't run a login.
|
||||
//
|
||||
cancel(req, rsp);
|
||||
return;
|
||||
}
|
||||
final String returnTo = req.getParameter("openid.return_to");
|
||||
if (returnTo != null && returnTo.contains("openid.rpnonce=")) {
|
||||
// Some providers (claimid.com) seem to embed these request
|
||||
// parameters into our return_to URL, and then give us them
|
||||
// in the return_to request parameter. But not all.
|
||||
//
|
||||
state.retTo.put("openid.rpnonce", req.getParameter("openid.rpnonce"));
|
||||
state.retTo.put("openid.rpsig", req.getParameter("openid.rpsig"));
|
||||
}
|
||||
|
||||
final String returnTo = req.getParameter("openid.return_to");
|
||||
if (returnTo != null && returnTo.contains("openid.rpnonce=")) {
|
||||
// Some providers (claimid.com) seem to embed these request
|
||||
// parameters into our return_to URL, and then give us them
|
||||
// in the return_to request parameter. But not all.
|
||||
//
|
||||
state.retTo.put("openid.rpnonce", req.getParameter("openid.rpnonce"));
|
||||
state.retTo.put("openid.rpsig", req.getParameter("openid.rpsig"));
|
||||
}
|
||||
|
||||
final VerificationResult result =
|
||||
manager.verify(state.retTo.toString(), new ParameterList(req
|
||||
.getParameterMap()), state.discovered);
|
||||
final Identifier user = result.getVerifiedId();
|
||||
|
||||
if (user != null) {
|
||||
// Authentication was successful.
|
||||
//
|
||||
final Message authRsp = result.getAuthResponse();
|
||||
SRegResponse sregRsp = null;
|
||||
FetchResponse fetchRsp = null;
|
||||
|
||||
if (authRsp.hasExtension(SRegMessage.OPENID_NS_SREG)) {
|
||||
final MessageExtension ext =
|
||||
authRsp.getExtension(SRegMessage.OPENID_NS_SREG);
|
||||
if (ext instanceof SRegResponse) {
|
||||
sregRsp = (SRegResponse) ext;
|
||||
}
|
||||
}
|
||||
|
||||
if (authRsp.hasExtension(AxMessage.OPENID_NS_AX)) {
|
||||
final MessageExtension ext =
|
||||
authRsp.getExtension(AxMessage.OPENID_NS_AX);
|
||||
if (ext instanceof FetchResponse) {
|
||||
fetchRsp = (FetchResponse) ext;
|
||||
}
|
||||
}
|
||||
|
||||
String fullname = null;
|
||||
String email = null;
|
||||
|
||||
if (sregRsp != null) {
|
||||
fullname = sregRsp.getAttributeValue("fullname");
|
||||
email = sregRsp.getAttributeValue("email");
|
||||
|
||||
} else if (fetchRsp != null) {
|
||||
final String firstName = fetchRsp.getAttributeValue("FirstName");
|
||||
final String lastName = fetchRsp.getAttributeValue("LastName");
|
||||
final StringBuilder n = new StringBuilder();
|
||||
if (firstName != null && firstName.length() > 0) {
|
||||
n.append(firstName);
|
||||
}
|
||||
if (lastName != null && lastName.length() > 0) {
|
||||
if (n.length() > 0) {
|
||||
n.append(' ');
|
||||
}
|
||||
n.append(lastName);
|
||||
}
|
||||
fullname = n.length() > 0 ? n.toString() : null;
|
||||
email = fetchRsp.getAttributeValue("Email");
|
||||
}
|
||||
|
||||
initializeAccount(req, rsp, user, fullname, email);
|
||||
} else if ("Nonce verification failed.".equals(result.getStatusMsg())) {
|
||||
final VerificationResult result =
|
||||
manager.verify(state.retTo.toString(), new ParameterList(req
|
||||
.getParameterMap()), state.discovered);
|
||||
final Identifier user = result.getVerifiedId();
|
||||
if (user == null /* authentication failure */) {
|
||||
if ("Nonce verification failed.".equals(result.getStatusMsg())) {
|
||||
// We might be suffering from clock skew on this system.
|
||||
//
|
||||
log.error("OpenID failure: " + result.getStatusMsg()
|
||||
+ " Likely caused by clock skew on this server,"
|
||||
+ " install/configure NTP.");
|
||||
cancelWithError(req, rsp, mode, result.getStatusMsg());
|
||||
cancelWithError(req, rsp, result.getStatusMsg());
|
||||
|
||||
} else if (result.getStatusMsg() != null) {
|
||||
// Authentication failed.
|
||||
//
|
||||
log.error("OpenID failure: " + result.getStatusMsg());
|
||||
cancelWithError(req, rsp, mode, result.getStatusMsg());
|
||||
cancelWithError(req, rsp, result.getStatusMsg());
|
||||
|
||||
} else {
|
||||
// Assume authentication was canceled.
|
||||
//
|
||||
cancel(req, rsp);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void debugRequest(final HttpServletRequest req) {
|
||||
System.err.println(req.getMethod() + " /" + RETURN_URL);
|
||||
for (final String n : new TreeMap<String, Object>(req.getParameterMap())
|
||||
.keySet()) {
|
||||
for (final String v : req.getParameterValues(n)) {
|
||||
System.err.println(" " + n + "=" + v);
|
||||
final Message authRsp = result.getAuthResponse();
|
||||
SRegResponse sregRsp = null;
|
||||
FetchResponse fetchRsp = null;
|
||||
|
||||
if (authRsp.hasExtension(SRegMessage.OPENID_NS_SREG)) {
|
||||
final MessageExtension ext =
|
||||
authRsp.getExtension(SRegMessage.OPENID_NS_SREG);
|
||||
if (ext instanceof SRegResponse) {
|
||||
sregRsp = (SRegResponse) ext;
|
||||
}
|
||||
}
|
||||
System.err.println();
|
||||
}
|
||||
|
||||
private void initializeAccount(final HttpServletRequest req,
|
||||
final HttpServletResponse rsp, final Identifier user,
|
||||
final String fullname, final String email) throws IOException {
|
||||
final SignInDialog.Mode mode = signInMode(req);
|
||||
Account account = null;
|
||||
boolean isNew = false;
|
||||
if (user != null) {
|
||||
try {
|
||||
final ReviewDb d = schema.open();
|
||||
try {
|
||||
switch (mode) {
|
||||
case SIGN_IN:
|
||||
case REGISTER: {
|
||||
SignInResult r = openAccount(d, user, fullname, email);
|
||||
if (r != null) {
|
||||
account = r.account;
|
||||
isNew = r.isNew;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LINK_IDENTIY:
|
||||
account = linkAccount(req, d, user, email);
|
||||
break;
|
||||
}
|
||||
} finally {
|
||||
d.close();
|
||||
if (authRsp.hasExtension(AxMessage.OPENID_NS_AX)) {
|
||||
final MessageExtension ext = authRsp.getExtension(AxMessage.OPENID_NS_AX);
|
||||
if (ext instanceof FetchResponse) {
|
||||
fetchRsp = (FetchResponse) ext;
|
||||
}
|
||||
}
|
||||
|
||||
final com.google.gerrit.server.account.AuthRequest areq =
|
||||
new com.google.gerrit.server.account.AuthRequest(user.getIdentifier());
|
||||
|
||||
if (sregRsp != null) {
|
||||
areq.setDisplayName(sregRsp.getAttributeValue("fullname"));
|
||||
areq.setEmailAddress(sregRsp.getAttributeValue("email"));
|
||||
|
||||
} else if (fetchRsp != null) {
|
||||
final String firstName = fetchRsp.getAttributeValue("FirstName");
|
||||
final String lastName = fetchRsp.getAttributeValue("LastName");
|
||||
final StringBuilder n = new StringBuilder();
|
||||
if (firstName != null && firstName.length() > 0) {
|
||||
n.append(firstName);
|
||||
}
|
||||
if (lastName != null && lastName.length() > 0) {
|
||||
if (n.length() > 0) {
|
||||
n.append(' ');
|
||||
}
|
||||
} catch (OrmException e) {
|
||||
log.error("Account lookup failed", e);
|
||||
account = null;
|
||||
n.append(lastName);
|
||||
}
|
||||
areq.setDisplayName(n.length() > 0 ? n.toString() : null);
|
||||
areq.setEmailAddress(fetchRsp.getAttributeValue("Email"));
|
||||
}
|
||||
|
||||
if (account == null) {
|
||||
if (isSignIn(mode)) {
|
||||
callFactory.get().logout();
|
||||
try {
|
||||
switch (mode) {
|
||||
case REGISTER:
|
||||
case SIGN_IN:
|
||||
final com.google.gerrit.server.account.AuthResult arsp;
|
||||
arsp = accountManager.authenticate(areq);
|
||||
|
||||
final Cookie lastId = new Cookie(OpenIdUtil.LASTID_COOKIE, "");
|
||||
lastId.setPath(req.getContextPath() + "/");
|
||||
if (remember) {
|
||||
lastId.setValue(user.getIdentifier());
|
||||
lastId.setMaxAge(LASTID_AGE);
|
||||
} else {
|
||||
lastId.setMaxAge(0);
|
||||
}
|
||||
rsp.addCookie(lastId);
|
||||
callFactory.get().setAccount(arsp.getAccountId(), remember);
|
||||
callback(arsp.isNew(), req, rsp);
|
||||
break;
|
||||
|
||||
case LINK_IDENTIY:
|
||||
accountManager.link(identifiedUser.get().getAccountId(), areq);
|
||||
callback(false, req, rsp);
|
||||
break;
|
||||
}
|
||||
cancel(req, rsp);
|
||||
|
||||
} else if (isSignIn(mode)) {
|
||||
final boolean remember = "1".equals(req.getParameter(P_REMEMBER));
|
||||
callFactory.get().setAccount(account.getId(), false);
|
||||
|
||||
final Cookie lastId = new Cookie(OpenIdUtil.LASTID_COOKIE, "");
|
||||
lastId.setPath(req.getContextPath() + "/");
|
||||
if (remember) {
|
||||
lastId.setValue(user.getIdentifier());
|
||||
lastId.setMaxAge(LASTID_AGE);
|
||||
} else {
|
||||
lastId.setMaxAge(0);
|
||||
}
|
||||
rsp.addCookie(lastId);
|
||||
|
||||
callback(isNew, req, rsp);
|
||||
|
||||
} else {
|
||||
callback(isNew, req, rsp);
|
||||
} catch (AccountException e) {
|
||||
log.error("OpenID authentication failure", e);
|
||||
cancelWithError(req, rsp, "Contact site administrator");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,104 +321,6 @@ class OpenIdServiceImpl implements OpenIdService {
|
||||
}
|
||||
}
|
||||
|
||||
static class SignInResult {
|
||||
final Account account;
|
||||
final boolean isNew;
|
||||
|
||||
SignInResult(final Account a, final boolean n) {
|
||||
account = a;
|
||||
isNew = n;
|
||||
}
|
||||
}
|
||||
|
||||
private SignInResult openAccount(final ReviewDb db, final Identifier user,
|
||||
final String fullname, final String email) throws OrmException {
|
||||
Account account;
|
||||
final AccountExternalIdAccess extAccess = db.accountExternalIds();
|
||||
AccountExternalId acctExt = lookup(extAccess, user.getIdentifier());
|
||||
|
||||
if (acctExt == null && email != null
|
||||
&& authConfig.isAllowGoogleAccountUpgrade() && isGoogleAccount(user)) {
|
||||
acctExt = lookupGoogleAccount(extAccess, email);
|
||||
if (acctExt != null) {
|
||||
// Legacy user from Gerrit 1? Attach the OpenID identity.
|
||||
//
|
||||
final AccountExternalId openidExt =
|
||||
new AccountExternalId(new AccountExternalId.Key(acctExt
|
||||
.getAccountId(), user.getIdentifier()));
|
||||
extAccess.insert(Collections.singleton(openidExt));
|
||||
acctExt = openidExt;
|
||||
}
|
||||
}
|
||||
|
||||
if (acctExt != null) {
|
||||
// Existing user; double check the email is current.
|
||||
//
|
||||
if (email != null && !email.equals(acctExt.getEmailAddress())) {
|
||||
byEmailCache.evict(acctExt.getEmailAddress());
|
||||
byEmailCache.evict(email);
|
||||
acctExt.setEmailAddress(email);
|
||||
}
|
||||
acctExt.setLastUsedOn();
|
||||
extAccess.update(Collections.singleton(acctExt));
|
||||
account = byIdCache.get(acctExt.getAccountId()).getAccount();
|
||||
} else {
|
||||
account = null;
|
||||
}
|
||||
|
||||
if (account == null) {
|
||||
// New user; create an account entity for them.
|
||||
//
|
||||
final Transaction txn = db.beginTransaction();
|
||||
|
||||
account = new Account(new Account.Id(db.nextAccountId()));
|
||||
account.setFullName(fullname);
|
||||
account.setPreferredEmail(email);
|
||||
|
||||
acctExt =
|
||||
new AccountExternalId(new AccountExternalId.Key(account.getId(), user
|
||||
.getIdentifier()));
|
||||
acctExt.setLastUsedOn();
|
||||
acctExt.setEmailAddress(email);
|
||||
|
||||
db.accounts().insert(Collections.singleton(account), txn);
|
||||
extAccess.insert(Collections.singleton(acctExt), txn);
|
||||
txn.commit();
|
||||
byEmailCache.evict(email);
|
||||
return new SignInResult(account, true);
|
||||
}
|
||||
return account != null ? new SignInResult(account, false) : null;
|
||||
}
|
||||
|
||||
private Account linkAccount(final HttpServletRequest req, final ReviewDb db,
|
||||
final Identifier user, final String curEmail) throws OrmException {
|
||||
final Account account = identifiedUser.get().getAccount();
|
||||
final AccountExternalId.Key idKey =
|
||||
new AccountExternalId.Key(account.getId(), user.getIdentifier());
|
||||
AccountExternalId id = db.accountExternalIds().get(idKey);
|
||||
if (id == null) {
|
||||
id = new AccountExternalId(idKey);
|
||||
id.setLastUsedOn();
|
||||
id.setEmailAddress(curEmail);
|
||||
db.accountExternalIds().insert(Collections.singleton(id));
|
||||
byEmailCache.evict(curEmail);
|
||||
byIdCache.evict(account.getId());
|
||||
} else {
|
||||
final String oldEmail = id.getEmailAddress();
|
||||
if (curEmail != null && !curEmail.equals(oldEmail)) {
|
||||
id.setEmailAddress(curEmail);
|
||||
}
|
||||
id.setLastUsedOn();
|
||||
db.accountExternalIds().update(Collections.singleton(id));
|
||||
if (curEmail != null && !curEmail.equals(oldEmail)) {
|
||||
byEmailCache.evict(oldEmail);
|
||||
byEmailCache.evict(curEmail);
|
||||
byIdCache.evict(account.getId());
|
||||
}
|
||||
}
|
||||
return account;
|
||||
}
|
||||
|
||||
private static Mode signInMode(final HttpServletRequest req) {
|
||||
try {
|
||||
return SignInDialog.Mode.valueOf(req.getParameter(P_MODE));
|
||||
@@ -517,44 +329,6 @@ class OpenIdServiceImpl implements OpenIdService {
|
||||
}
|
||||
}
|
||||
|
||||
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 Identifier user) {
|
||||
return user.getIdentifier().startsWith(OpenIdUtil.URL_GOOGLE + "?");
|
||||
}
|
||||
|
||||
private static AccountExternalId lookupGoogleAccount(
|
||||
final AccountExternalIdAccess extAccess, final String email)
|
||||
throws OrmException {
|
||||
// We may have multiple records which match the email address, but
|
||||
// all under the same account. This happens when the user does a
|
||||
// login through different server hostnames, as Google issues
|
||||
// unique OpenID tokens per server.
|
||||
//
|
||||
// Match to an existing account only if there is exactly one record
|
||||
// for this email using the generic Google identity.
|
||||
//
|
||||
final List<AccountExternalId> m = new ArrayList<AccountExternalId>();
|
||||
for (final AccountExternalId e : extAccess.byEmailAddress(email)) {
|
||||
if (e.getExternalId().equals(AccountExternalId.LEGACY_GAE + email)) {
|
||||
m.add(e);
|
||||
}
|
||||
}
|
||||
return m.size() == 1 ? m.get(0) : null;
|
||||
}
|
||||
|
||||
private void callback(final boolean isNew, final HttpServletRequest req,
|
||||
final HttpServletResponse rsp) throws IOException {
|
||||
String token = req.getParameter(P_TOKEN);
|
||||
@@ -575,12 +349,19 @@ class OpenIdServiceImpl implements OpenIdService {
|
||||
|
||||
private void cancel(final HttpServletRequest req,
|
||||
final HttpServletResponse rsp) throws IOException {
|
||||
if (isSignIn(signInMode(req))) {
|
||||
callFactory.get().logout();
|
||||
}
|
||||
callback(false, req, rsp);
|
||||
}
|
||||
|
||||
private void cancelWithError(final HttpServletRequest req,
|
||||
final HttpServletResponse rsp, final SignInDialog.Mode mode,
|
||||
final String errorDetail) throws IOException {
|
||||
final HttpServletResponse rsp, final String errorDetail)
|
||||
throws IOException {
|
||||
final SignInDialog.Mode mode = signInMode(req);
|
||||
if (isSignIn(mode)) {
|
||||
callFactory.get().logout();
|
||||
}
|
||||
final StringBuilder rdr = new StringBuilder();
|
||||
rdr.append(urlProvider.get());
|
||||
rdr.append('#');
|
||||
|
||||
@@ -31,6 +31,9 @@ import com.google.gerrit.server.ContactStore;
|
||||
import com.google.gerrit.server.CurrentUser;
|
||||
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.config.AuthConfig;
|
||||
import com.google.gerrit.server.mail.EmailException;
|
||||
import com.google.gerrit.server.mail.RegisterNewEmailSender;
|
||||
@@ -40,7 +43,6 @@ import com.google.gwt.user.client.rpc.AsyncCallback;
|
||||
import com.google.gwtjsonrpc.client.VoidResult;
|
||||
import com.google.gwtjsonrpc.server.ValidToken;
|
||||
import com.google.gwtjsonrpc.server.XsrfException;
|
||||
import com.google.gwtorm.client.OrmDuplicateKeyException;
|
||||
import com.google.gwtorm.client.OrmException;
|
||||
import com.google.gwtorm.client.Transaction;
|
||||
import com.google.inject.Inject;
|
||||
@@ -70,6 +72,7 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
|
||||
private final SshKeyCache sshKeyCache;
|
||||
private final AccountByEmailCache byEmailCache;
|
||||
private final AccountCache accountCache;
|
||||
private final AccountManager accountManager;
|
||||
private final boolean useContactInfo;
|
||||
|
||||
private final ExternalIdDetailFactory.Factory externalIdDetailFactory;
|
||||
@@ -79,7 +82,7 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
|
||||
final Provider<CurrentUser> currentUser, final ContactStore cs,
|
||||
final AuthConfig ac, final RegisterNewEmailSender.Factory esf,
|
||||
final SshKeyCache skc, final AccountByEmailCache abec,
|
||||
final AccountCache uac,
|
||||
final AccountCache uac, final AccountManager am,
|
||||
final ExternalIdDetailFactory.Factory externalIdDetailFactory) {
|
||||
super(schema, currentUser);
|
||||
contactStore = cs;
|
||||
@@ -88,6 +91,7 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
|
||||
sshKeyCache = skc;
|
||||
byEmailCache = abec;
|
||||
accountCache = uac;
|
||||
accountManager = am;
|
||||
|
||||
useContactInfo = contactStore != null && contactStore.isEnabled();
|
||||
|
||||
@@ -341,7 +345,6 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
|
||||
|
||||
public void validateEmail(final String token,
|
||||
final AsyncCallback<VoidResult> callback) {
|
||||
final String newEmail;
|
||||
try {
|
||||
final ValidToken t =
|
||||
authConfig.getEmailRegistrationToken().checkToken(token, null);
|
||||
@@ -349,48 +352,19 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
|
||||
callback.onFailure(new IllegalStateException("Invalid token"));
|
||||
return;
|
||||
}
|
||||
newEmail = new String(Base64.decode(t.getData()), "UTF-8");
|
||||
final String newEmail = new String(Base64.decode(t.getData()), "UTF-8");
|
||||
if (!newEmail.contains("@")) {
|
||||
callback.onFailure(new IllegalStateException("Invalid token"));
|
||||
return;
|
||||
}
|
||||
accountManager.link(getAccountId(), AuthRequest.forEmail(newEmail));
|
||||
callback.onSuccess(VoidResult.INSTANCE);
|
||||
} catch (XsrfException e) {
|
||||
callback.onFailure(e);
|
||||
return;
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
callback.onFailure(e);
|
||||
return;
|
||||
} catch (AccountException e) {
|
||||
callback.onFailure(e);
|
||||
}
|
||||
|
||||
run(callback, new Action<VoidResult>() {
|
||||
public VoidResult run(ReviewDb db) throws OrmException {
|
||||
final Account.Id me = getAccountId();
|
||||
final List<AccountExternalId> exists =
|
||||
db.accountExternalIds().byAccountEmail(me, newEmail).toList();
|
||||
if (!exists.isEmpty()) {
|
||||
return VoidResult.INSTANCE;
|
||||
}
|
||||
|
||||
try {
|
||||
final AccountExternalId id =
|
||||
new AccountExternalId(new AccountExternalId.Key(me,
|
||||
AccountExternalId.SCHEME_MAILTO + newEmail));
|
||||
id.setEmailAddress(newEmail);
|
||||
db.accountExternalIds().insert(Collections.singleton(id));
|
||||
} catch (OrmDuplicateKeyException e) {
|
||||
// Ignore a duplicate registration
|
||||
}
|
||||
|
||||
final Account a = db.accounts().get(me);
|
||||
final String oldEmail = a.getPreferredEmail();
|
||||
a.setPreferredEmail(newEmail);
|
||||
db.accounts().update(Collections.singleton(a));
|
||||
|
||||
byEmailCache.evict(oldEmail);
|
||||
byEmailCache.evict(newEmail);
|
||||
accountCache.evict(me);
|
||||
return VoidResult.INSTANCE;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user