Allow users to register additional email addresses
The SSH username cannot be set without having an email address, and some types of CLAs may require an email address to be set in the contact information for the user. However, not all OpenID providers track or transmit emails to relying parties, so users must still be able to register an address externally from their OpenID system. We now send out a validation email and use a secure token to verify the address matches where the user sent it to. A "mailto:" URL is used in the external id table to register the additional address, thus making it available as a choice for the user in the settings. Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
@@ -16,6 +16,7 @@ package com.google.gerrit.client;
|
||||
|
||||
import com.google.gerrit.client.account.AccountSettings;
|
||||
import com.google.gerrit.client.account.NewAgreementScreen;
|
||||
import com.google.gerrit.client.account.ValidateEmailScreen;
|
||||
import com.google.gerrit.client.admin.AccountGroupScreen;
|
||||
import com.google.gerrit.client.admin.GroupListScreen;
|
||||
import com.google.gerrit.client.admin.ProjectAdminScreen;
|
||||
@@ -167,6 +168,11 @@ public class Link implements HistoryListener {
|
||||
}
|
||||
}
|
||||
|
||||
p = "VE,";
|
||||
if (token.startsWith(p)) {
|
||||
return new ValidateEmailScreen(skip(p, token));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@@ -59,6 +59,10 @@ public interface AccountConstants extends Constants {
|
||||
String contactFieldPhone();
|
||||
String contactFieldFax();
|
||||
String buttonSaveContact();
|
||||
String buttonOpenRegisterNewEmail();
|
||||
String buttonSendRegisterNewEmail();
|
||||
String titleRegisterNewEmail();
|
||||
String descRegisterNewEmail();
|
||||
|
||||
String newAgreement();
|
||||
String agreementStatus();
|
||||
|
@@ -40,6 +40,13 @@ contactFieldCountry = Country
|
||||
contactFieldPhone = Phone Number
|
||||
contactFieldFax = Fax Number
|
||||
buttonSaveContact = Save
|
||||
buttonOpenRegisterNewEmail = Register New Email ...
|
||||
buttonSendRegisterNewEmail = Register
|
||||
titleRegisterNewEmail = Register Email Address
|
||||
descRegisterNewEmail = \
|
||||
<p>A confirmation link will be sent by email to this address.</p>\
|
||||
<p>You must click on the link to complete the registration and make the address available for selection.</p>
|
||||
|
||||
|
||||
newAgreement = New Contributor Agreement
|
||||
agreementStatus = Status
|
||||
|
@@ -47,4 +47,10 @@ public interface AccountSecurity extends RemoteJsonService {
|
||||
@SignInRequired
|
||||
void enterAgreement(ContributorAgreement.Id id,
|
||||
AsyncCallback<VoidResult> callback);
|
||||
|
||||
@SignInRequired
|
||||
void registerEmail(String address, AsyncCallback<VoidResult> callback);
|
||||
|
||||
@SignInRequired
|
||||
void validateEmail(String token, AsyncCallback<VoidResult> callback);
|
||||
}
|
||||
|
@@ -19,6 +19,7 @@ import com.google.gerrit.client.reviewdb.Account;
|
||||
import com.google.gerrit.client.reviewdb.AccountExternalId;
|
||||
import com.google.gerrit.client.reviewdb.ContactInformation;
|
||||
import com.google.gerrit.client.rpc.GerritCallback;
|
||||
import com.google.gerrit.client.ui.AutoCenterDialogBox;
|
||||
import com.google.gerrit.client.ui.TextSaveButtonListener;
|
||||
import com.google.gwt.i18n.client.LocaleInfo;
|
||||
import com.google.gwt.user.client.ui.Button;
|
||||
@@ -26,10 +27,16 @@ import com.google.gwt.user.client.ui.ChangeListener;
|
||||
import com.google.gwt.user.client.ui.ClickListener;
|
||||
import com.google.gwt.user.client.ui.Composite;
|
||||
import com.google.gwt.user.client.ui.FlowPanel;
|
||||
import com.google.gwt.user.client.ui.FormHandler;
|
||||
import com.google.gwt.user.client.ui.FormPanel;
|
||||
import com.google.gwt.user.client.ui.FormSubmitCompleteEvent;
|
||||
import com.google.gwt.user.client.ui.FormSubmitEvent;
|
||||
import com.google.gwt.user.client.ui.Grid;
|
||||
import com.google.gwt.user.client.ui.HTML;
|
||||
import com.google.gwt.user.client.ui.ListBox;
|
||||
import com.google.gwt.user.client.ui.TextArea;
|
||||
import com.google.gwt.user.client.ui.TextBox;
|
||||
import com.google.gwt.user.client.ui.VerticalPanel;
|
||||
import com.google.gwt.user.client.ui.Widget;
|
||||
import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
|
||||
import com.google.gwtjsonrpc.client.VoidResult;
|
||||
@@ -51,6 +58,7 @@ class ContactPanel extends Composite {
|
||||
|
||||
private TextBox nameTxt;
|
||||
private ListBox emailPick;
|
||||
private Button registerNewEmail;
|
||||
private TextArea addressTxt;
|
||||
private TextBox countryTxt;
|
||||
private TextBox phoneTxt;
|
||||
@@ -99,8 +107,19 @@ class ContactPanel extends Composite {
|
||||
info.addStyleName("gerrit-AccountInfoBlock");
|
||||
body.add(info);
|
||||
|
||||
registerNewEmail = new Button(Util.C.buttonOpenRegisterNewEmail());
|
||||
registerNewEmail.setEnabled(false);
|
||||
registerNewEmail.addClickListener(new ClickListener() {
|
||||
public void onClick(final Widget sender) {
|
||||
doRegisterNewEmail();
|
||||
}
|
||||
});
|
||||
final FlowPanel emailLine = new FlowPanel();
|
||||
emailLine.add(emailPick);
|
||||
emailLine.add(registerNewEmail);
|
||||
|
||||
row(0, Util.C.contactFieldFullName(), nameTxt);
|
||||
row(1, Util.C.contactFieldEmail(), emailPick);
|
||||
row(1, Util.C.contactFieldEmail(), emailLine);
|
||||
row(2, Util.C.contactFieldAddress(), addressTxt);
|
||||
row(3, Util.C.contactFieldCountry(), countryTxt);
|
||||
row(4, Util.C.contactFieldPhone(), phoneTxt);
|
||||
@@ -124,7 +143,17 @@ class ContactPanel extends Composite {
|
||||
nameTxt.addKeyboardListener(sbl);
|
||||
emailPick.addChangeListener(new ChangeListener() {
|
||||
public void onChange(Widget sender) {
|
||||
save.setEnabled(true);
|
||||
final int idx = emailPick.getSelectedIndex();
|
||||
final String v = 0 <= idx ? emailPick.getValue(idx) : null;
|
||||
if (Util.C.buttonOpenRegisterNewEmail().equals(v)) {
|
||||
for (int i = 0; i < emailPick.getItemCount(); i++) {
|
||||
if (currentEmail.equals(emailPick.getValue(i))) {
|
||||
emailPick.setSelectedIndex(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
doRegisterNewEmail();
|
||||
}
|
||||
}
|
||||
});
|
||||
addressTxt.addKeyboardListener(sbl);
|
||||
@@ -146,6 +175,7 @@ class ContactPanel extends Composite {
|
||||
|
||||
emailPick.clear();
|
||||
emailPick.setEnabled(false);
|
||||
registerNewEmail.setEnabled(false);
|
||||
|
||||
haveAccount = false;
|
||||
haveEmails = false;
|
||||
@@ -200,7 +230,15 @@ class ContactPanel extends Composite {
|
||||
emailPick.setSelectedIndex(emailPick.getItemCount() - 1);
|
||||
}
|
||||
}
|
||||
emailPick.setEnabled(true);
|
||||
if (emailPick.getItemCount() > 0) {
|
||||
emailPick.setVisible(true);
|
||||
emailPick.setEnabled(true);
|
||||
emailPick.addItem("... " + Util.C.buttonOpenRegisterNewEmail()
|
||||
+ " ", Util.C.buttonOpenRegisterNewEmail());
|
||||
} else {
|
||||
emailPick.setVisible(false);
|
||||
}
|
||||
registerNewEmail.setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,11 +263,69 @@ class ContactPanel extends Composite {
|
||||
save.setEnabled(false);
|
||||
}
|
||||
|
||||
private void doRegisterNewEmail() {
|
||||
final AutoCenterDialogBox box = new AutoCenterDialogBox(true, true);
|
||||
final VerticalPanel body = new VerticalPanel();
|
||||
|
||||
final TextBox inEmail = new TextBox();
|
||||
inEmail.setVisibleLength(60);
|
||||
|
||||
final Button register = new Button(Util.C.buttonSendRegisterNewEmail());
|
||||
final FormPanel form = new FormPanel();
|
||||
form.addFormHandler(new FormHandler() {
|
||||
public void onSubmit(final FormSubmitEvent event) {
|
||||
event.setCancelled(true);
|
||||
final String addr = inEmail.getText().trim();
|
||||
if (!addr.contains("@")) {
|
||||
return;
|
||||
}
|
||||
|
||||
inEmail.setEnabled(false);
|
||||
register.setEnabled(false);
|
||||
Util.ACCOUNT_SEC.registerEmail(addr, new GerritCallback<VoidResult>() {
|
||||
public void onSuccess(VoidResult result) {
|
||||
box.hide();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(final Throwable caught) {
|
||||
inEmail.setEnabled(true);
|
||||
register.setEnabled(true);
|
||||
super.onFailure(caught);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void onSubmitComplete(final FormSubmitCompleteEvent event) {
|
||||
}
|
||||
});
|
||||
form.setWidget(body);
|
||||
|
||||
register.addClickListener(new ClickListener() {
|
||||
public void onClick(Widget sender) {
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
body.add(new HTML(Util.C.descRegisterNewEmail()));
|
||||
body.add(inEmail);
|
||||
body.add(register);
|
||||
|
||||
box.setText(Util.C.titleRegisterNewEmail());
|
||||
box.setWidget(form);
|
||||
box.center();
|
||||
inEmail.setFocus(true);
|
||||
}
|
||||
|
||||
void doSave() {
|
||||
final String newName = nameTxt.getText();
|
||||
final String newEmail;
|
||||
if (emailPick.isEnabled() && emailPick.getSelectedIndex() >= 0) {
|
||||
newEmail = emailPick.getValue(emailPick.getSelectedIndex());
|
||||
final String v = emailPick.getValue(emailPick.getSelectedIndex());
|
||||
if (Util.C.buttonOpenRegisterNewEmail().equals(v)) {
|
||||
newEmail = currentEmail;
|
||||
} else {
|
||||
newEmail = v;
|
||||
}
|
||||
} else {
|
||||
newEmail = currentEmail;
|
||||
}
|
||||
@@ -239,11 +335,13 @@ class ContactPanel extends Composite {
|
||||
info.setCountry(countryTxt.getText());
|
||||
info.setPhoneNumber(phoneTxt.getText());
|
||||
info.setFaxNumber(faxTxt.getText());
|
||||
save.setEnabled(false);
|
||||
registerNewEmail.setEnabled(false);
|
||||
|
||||
Util.ACCOUNT_SEC.updateContact(newName, newEmail, info,
|
||||
new GerritCallback<VoidResult>() {
|
||||
public void onSuccess(final VoidResult result) {
|
||||
save.setEnabled(false);
|
||||
registerNewEmail.setEnabled(false);
|
||||
final Account me = Gerrit.getUserAccount();
|
||||
me.setFullName(newName);
|
||||
me.setPreferredEmail(newEmail);
|
||||
@@ -253,6 +351,13 @@ class ContactPanel extends Composite {
|
||||
parentScreen.display(me);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(final Throwable caught) {
|
||||
save.setEnabled(true);
|
||||
registerNewEmail.setEnabled(true);
|
||||
super.onFailure(caught);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,41 @@
|
||||
// Copyright 2009 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.account;
|
||||
|
||||
import com.google.gerrit.client.Link;
|
||||
import com.google.gerrit.client.rpc.GerritCallback;
|
||||
import com.google.gerrit.client.ui.AccountScreen;
|
||||
import com.google.gwt.user.client.History;
|
||||
import com.google.gwtjsonrpc.client.VoidResult;
|
||||
|
||||
public class ValidateEmailScreen extends AccountScreen {
|
||||
private final String magicToken;
|
||||
|
||||
public ValidateEmailScreen(final String magicToken) {
|
||||
super(Util.C.accountSettingsHeading());
|
||||
this.magicToken = magicToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
super.onLoad();
|
||||
Util.ACCOUNT_SEC.validateEmail(magicToken,
|
||||
new GerritCallback<VoidResult>() {
|
||||
public void onSuccess(final VoidResult result) {
|
||||
History.newItem(Link.SETTINGS_CONTACT, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@@ -31,6 +31,10 @@ public interface AccountExternalIdAccess extends
|
||||
@Query("WHERE key.accountId = ?")
|
||||
ResultSet<AccountExternalId> byAccount(Account.Id id) throws OrmException;
|
||||
|
||||
@Query("WHERE key.accountId = ? AND emailAddress = ?")
|
||||
ResultSet<AccountExternalId> byAccountEmail(Account.Id id, String email)
|
||||
throws OrmException;
|
||||
|
||||
@Query("WHERE emailAddress = ?")
|
||||
ResultSet<AccountExternalId> byEmailAddress(String email) throws OrmException;
|
||||
|
||||
|
@@ -28,18 +28,39 @@ import com.google.gerrit.client.rpc.NoSuchEntityException;
|
||||
import com.google.gerrit.server.ssh.SshUtil;
|
||||
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 org.spearce.jgit.lib.PersonIdent;
|
||||
import org.spearce.jgit.util.Base64;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class AccountSecurityImpl extends BaseServiceImplementation implements
|
||||
import javax.mail.Message;
|
||||
import javax.mail.MessagingException;
|
||||
import javax.mail.Transport;
|
||||
import javax.mail.internet.InternetAddress;
|
||||
import javax.mail.internet.MimeMessage;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
class AccountSecurityImpl extends BaseServiceImplementation implements
|
||||
AccountSecurity {
|
||||
private final GerritServer server;
|
||||
|
||||
AccountSecurityImpl(final GerritServer gs) {
|
||||
server = gs;
|
||||
}
|
||||
|
||||
public void mySshKeys(final AsyncCallback<List<AccountSshKey>> callback) {
|
||||
run(callback, new Action<List<AccountSshKey>>() {
|
||||
public List<AccountSshKey> run(ReviewDb db) throws OrmException {
|
||||
@@ -148,4 +169,127 @@ public class AccountSecurityImpl extends BaseServiceImplementation implements
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void registerEmail(final String address,
|
||||
final AsyncCallback<VoidResult> cb) {
|
||||
final PersonIdent gi = server.newGerritPersonIdent();
|
||||
final HttpServletRequest req =
|
||||
GerritJsonServlet.getCurrentCall().getHttpServletRequest();
|
||||
final StringBuffer url = req.getRequestURL();
|
||||
final StringBuilder m = new StringBuilder();
|
||||
|
||||
url.setLength(url.lastIndexOf("/")); // cut "AccountSecurity"
|
||||
url.setLength(url.lastIndexOf("/")); // cut "rpc"
|
||||
url.append("/Gerrit#VE,");
|
||||
|
||||
try {
|
||||
url.append(server.getEmailRegistrationToken().newToken(
|
||||
Base64.encodeBytes(address.getBytes("UTF-8"))));
|
||||
} catch (XsrfException e) {
|
||||
cb.onFailure(e);
|
||||
return;
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
cb.onFailure(e);
|
||||
return;
|
||||
}
|
||||
|
||||
m.append("Welcome to Gerrit Code Review at ");
|
||||
m.append(req.getServerName());
|
||||
m.append(".\n");
|
||||
|
||||
m.append("\n");
|
||||
m.append("To add a verified email address to your user account, please\n");
|
||||
m.append("click on the following link:\n");
|
||||
m.append("\n");
|
||||
m.append(url);
|
||||
m.append("\n");
|
||||
|
||||
m.append("\n");
|
||||
m.append("If you have received this mail in error,"
|
||||
+ " you do not need to take any\n");
|
||||
m.append("action to cancel the account."
|
||||
+ " The account will not be activated, and\n");
|
||||
m.append("you will not receive any further emails.\n");
|
||||
|
||||
m.append("\n");
|
||||
m.append("If clicking the link above does not work,"
|
||||
+ " copy and paste the URL in a\n");
|
||||
m.append("new browser window instead.\n");
|
||||
|
||||
m.append("\n");
|
||||
m.append("This is a send-only email address."
|
||||
+ " Replies to this message will not\n");
|
||||
m.append("be read or answered.\n");
|
||||
|
||||
final javax.mail.Session out = server.getOutgoingMail();
|
||||
if (out == null) {
|
||||
cb.onFailure(new IllegalStateException("Outgoing mail is disabled"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final MimeMessage msg = new MimeMessage(out);
|
||||
msg.setFrom(new InternetAddress(gi.getEmailAddress(), gi.getName()));
|
||||
msg.setRecipients(Message.RecipientType.TO, address);
|
||||
msg.setSubject("[Gerrit Code Review] Email Verification");
|
||||
msg.setSentDate(new Date());
|
||||
msg.setText(m.toString());
|
||||
Transport.send(msg);
|
||||
cb.onSuccess(VoidResult.INSTANCE);
|
||||
} catch (MessagingException e) {
|
||||
cb.onFailure(e);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
cb.onFailure(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void validateEmail(final String token,
|
||||
final AsyncCallback<VoidResult> callback) {
|
||||
final String address;
|
||||
try {
|
||||
final ValidToken t =
|
||||
server.getEmailRegistrationToken().checkToken(token, null);
|
||||
if (t == null || t.getData() == null || "".equals(t.getData())) {
|
||||
callback.onFailure(new IllegalStateException("Invalid token"));
|
||||
return;
|
||||
}
|
||||
address = new String(Base64.decode(t.getData()), "UTF-8");
|
||||
if (!address.contains("@")) {
|
||||
callback.onFailure(new IllegalStateException("Invalid token"));
|
||||
return;
|
||||
}
|
||||
} catch (XsrfException e) {
|
||||
callback.onFailure(e);
|
||||
return;
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
callback.onFailure(e);
|
||||
return;
|
||||
}
|
||||
|
||||
run(callback, new Action<VoidResult>() {
|
||||
public VoidResult run(ReviewDb db) throws OrmException {
|
||||
final Account.Id me = Common.getAccountId();
|
||||
final List<AccountExternalId> exists =
|
||||
db.accountExternalIds().byAccountEmail(me, address).toList();
|
||||
if (!exists.isEmpty()) {
|
||||
return VoidResult.INSTANCE;
|
||||
}
|
||||
|
||||
try {
|
||||
final AccountExternalId id =
|
||||
new AccountExternalId(new AccountExternalId.Key(me, "mailto:"
|
||||
+ address));
|
||||
id.setEmailAddress(address);
|
||||
db.accountExternalIds().insert(Collections.singleton(id));
|
||||
} catch (OrmDuplicateKeyException e) {
|
||||
// Ignore a duplicate registration
|
||||
}
|
||||
|
||||
final Account a = db.accounts().get(me);
|
||||
a.setPreferredEmail(address);
|
||||
db.accounts().update(Collections.singleton(a));
|
||||
Common.getAccountCache().invalidate(me);
|
||||
return VoidResult.INSTANCE;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -19,6 +19,6 @@ package com.google.gerrit.server;
|
||||
public class AccountSecuritySrv extends GerritJsonServlet {
|
||||
@Override
|
||||
protected Object createServiceHandle() throws Exception {
|
||||
return new AccountSecurityImpl();
|
||||
return new AccountSecurityImpl(GerritServer.getInstance());
|
||||
}
|
||||
}
|
||||
|
@@ -89,6 +89,7 @@ public class GerritServer {
|
||||
private final PersonIdent gerritPersonIdentTemplate;
|
||||
private final SignedToken xsrf;
|
||||
private final SignedToken account;
|
||||
private final SignedToken emailReg;
|
||||
private final RepositoryCache repositories;
|
||||
private final javax.mail.Session outgoingMail;
|
||||
|
||||
@@ -112,6 +113,7 @@ public class GerritServer {
|
||||
break;
|
||||
}
|
||||
account = new SignedToken(accountCookieAge, sConfig.accountPrivateKey);
|
||||
emailReg = new SignedToken(5 * 24 * 60 * 60, sConfig.accountPrivateKey);
|
||||
|
||||
if (sConfig.gitBasePath != null) {
|
||||
repositories = new RepositoryCache(new File(sConfig.gitBasePath));
|
||||
@@ -129,7 +131,7 @@ public class GerritServer {
|
||||
}
|
||||
gerritPersonIdentTemplate = new PersonIdent(sConfig.gerritGitName, email);
|
||||
outgoingMail = createOutgoingMail();
|
||||
|
||||
|
||||
Common.setSchemaFactory(db);
|
||||
Common.setProjectCache(new ProjectCache());
|
||||
Common.setAccountCache(new AccountCache());
|
||||
@@ -399,6 +401,11 @@ public class GerritServer {
|
||||
return account;
|
||||
}
|
||||
|
||||
/** Get the signature used for email registration/validation links. */
|
||||
public SignedToken getEmailRegistrationToken() {
|
||||
return emailReg;
|
||||
}
|
||||
|
||||
public String getLoginHttpHeader() {
|
||||
return sConfig.loginHttpHeader;
|
||||
}
|
||||
|
Reference in New Issue
Block a user