diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt index 2b583aa0c0..9794666cf2 100644 --- a/Documentation/rest-api-accounts.txt +++ b/Documentation/rest-api-accounts.txt @@ -224,6 +224,38 @@ describes the email address. } ---- +[[create-account-email]] +Create Account Email +~~~~~~~~~~~~~~~~~~~~ +[verse] +'PUT /accounts/link:#account-id[\{account-id\}]/emails/link:#email-id[\{email-id\}]' + +Registers a new email address for the user. A verification email is +sent with a link that needs to be visited to confirm the email address, +unless `DEVELOPMENT_BECOME_ANY_ACCOUNT` is used as authentication type. +For the development mode email addresses are directly added without +confirmation. + +.Request +---- + PUT /accounts/self/emails/john.doe@example.com HTTP/1.0 +---- + +As response the new email address is returned as +link:#email-info[EmailInfo] entity. + +.Response +---- + HTTP/1.1 201 Created + Content-Disposition: attachment + Content-Type: application/json;charset=UTF-8 + + )]}' + { + "email": "john.doe@example.com" + } +---- + [[set-preferred-email]] Set Preferred Email ~~~~~~~~~~~~~~~~~~~ diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java index c2d03ab601..46ea9bacf8 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java @@ -75,10 +75,6 @@ public interface AccountSecurity extends RemoteJsonService { void enterAgreement(String agreementName, AsyncCallback callback); - @Audit - @SignInRequired - void registerEmail(String address, AsyncCallback callback); - @Audit @SignInRequired void validateEmail(String token, AsyncCallback callback); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java new file mode 100644 index 0000000000..70aba3d156 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java @@ -0,0 +1,34 @@ +// Copyright (C) 2013 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.client.account; + +import com.google.gerrit.client.rpc.NativeString; +import com.google.gerrit.client.rpc.RestApi; +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.user.client.rpc.AsyncCallback; + +/** + * A collection of static methods which work on the Gerrit REST API for specific + * accounts. + */ +public class AccountApi { + /** Register a new email address */ + public static void registerEmail(String account, String email, + AsyncCallback cb) { + JavaScriptObject in = JavaScriptObject.createObject(); + new RestApi("/accounts/").id(account).view("emails").id(email) + .ifNoneMatch().put(in, cb); + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java index f35bd4b430..053146b018 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java @@ -17,6 +17,7 @@ package com.google.gerrit.client.account; import com.google.gerrit.client.ErrorDialog; import com.google.gerrit.client.Gerrit; import com.google.gerrit.client.rpc.GerritCallback; +import com.google.gerrit.client.rpc.NativeString; import com.google.gerrit.client.ui.OnEditEnabler; import com.google.gerrit.common.PageLinks; import com.google.gerrit.common.errors.EmailException; @@ -283,13 +284,16 @@ class ContactPanelShort extends Composite { inEmail.setEnabled(false); register.setEnabled(false); - Util.ACCOUNT_SEC.registerEmail(addr, new GerritCallback() { - public void onSuccess(Account currentUser) { + AccountApi.registerEmail("self", addr, new GerritCallback() { + @Override + public void onSuccess(NativeString result) { box.hide(); if (Gerrit.getConfig().getAuthType() == AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT) { currentEmail = addr; if (emailPick.getItemCount() == 0) { - onSaveSuccess(currentUser); + final Account me = Gerrit.getUserAccount(); + me.setPreferredEmail(addr); + onSaveSuccess(me); } else { save.setEnabled(true); } diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java index bef6316738..d0fb5041f4 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java @@ -17,7 +17,6 @@ package com.google.gerrit.httpd.rpc.account; import com.google.gerrit.httpd.rpc.RpcServletModule; import com.google.gerrit.httpd.rpc.UiRpcModule; import com.google.gerrit.server.config.FactoryModule; -import com.google.gerrit.server.mail.RegisterNewEmailSender; public class AccountModule extends RpcServletModule { public AccountModule() { @@ -32,7 +31,6 @@ public class AccountModule extends RpcServletModule { factory(AgreementInfoFactory.Factory.class); factory(DeleteExternalIds.Factory.class); factory(ExternalIdDetailFactory.Factory.class); - factory(RegisterNewEmailSender.Factory.class); } }); rpc(AccountSecurityImpl.class); diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java index 6d183b8201..de1295c08f 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java @@ -19,7 +19,6 @@ import com.google.gerrit.common.ChangeHooks; import com.google.gerrit.common.data.AccountSecurity; import com.google.gerrit.common.data.ContributorAgreement; import com.google.gerrit.common.errors.ContactInformationStoreException; -import com.google.gerrit.common.errors.EmailException; import com.google.gerrit.common.errors.InvalidSshKeyException; import com.google.gerrit.common.errors.NoSuchEntityException; import com.google.gerrit.common.errors.PermissionDeniedException; @@ -31,7 +30,6 @@ import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.AccountGroupMember; import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit; import com.google.gerrit.reviewdb.client.AccountSshKey; -import com.google.gerrit.reviewdb.client.AuthType; import com.google.gerrit.reviewdb.client.ContactInformation; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; @@ -40,16 +38,13 @@ import com.google.gerrit.server.account.AccountByEmailCache; import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountException; import com.google.gerrit.server.account.AccountManager; -import com.google.gerrit.server.account.AuthRequest; import com.google.gerrit.server.account.ChangeUserName; import com.google.gerrit.server.account.ClearPassword; import com.google.gerrit.server.account.GeneratePassword; import com.google.gerrit.server.account.GroupCache; import com.google.gerrit.server.account.Realm; -import com.google.gerrit.server.config.AuthConfig; import com.google.gerrit.server.contact.ContactStore; import com.google.gerrit.server.mail.EmailTokenVerifier; -import com.google.gerrit.server.mail.RegisterNewEmailSender; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.ssh.SshKeyCache; import com.google.gwtjsonrpc.common.AsyncCallback; @@ -58,23 +53,17 @@ import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.util.Collections; import java.util.List; import java.util.Set; class AccountSecurityImpl extends BaseServiceImplementation implements AccountSecurity { - private final Logger log = LoggerFactory.getLogger(getClass()); private final ContactStore contactStore; - private final AuthConfig authConfig; private final Realm realm; private final ProjectCache projectCache; private final Provider user; private final EmailTokenVerifier emailTokenVerifier; - private final RegisterNewEmailSender.Factory registerNewEmailFactory; private final SshKeyCache sshKeyCache; private final AccountByEmailCache byEmailCache; private final AccountCache accountCache; @@ -93,11 +82,10 @@ class AccountSecurityImpl extends BaseServiceImplementation implements @Inject AccountSecurityImpl(final Provider schema, final Provider currentUser, final ContactStore cs, - final AuthConfig ac, final Realm r, final Provider u, + final Realm r, final Provider u, final EmailTokenVerifier etv, final ProjectCache pc, - final RegisterNewEmailSender.Factory esf, final SshKeyCache skc, - final AccountByEmailCache abec, final AccountCache uac, - final AccountManager am, + final SshKeyCache skc, final AccountByEmailCache abec, + final AccountCache uac, final AccountManager am, final ClearPassword.Factory clearPasswordFactory, final GeneratePassword.Factory generatePasswordFactory, final ChangeUserName.CurrentUser changeUserNameFactory, @@ -106,12 +94,10 @@ class AccountSecurityImpl extends BaseServiceImplementation implements final ChangeHooks hooks, final GroupCache groupCache) { super(schema, currentUser); contactStore = cs; - authConfig = ac; realm = r; user = u; emailTokenVerifier = etv; projectCache = pc; - registerNewEmailFactory = esf; sshKeyCache = skc; byEmailCache = abec; accountCache = uac; @@ -302,31 +288,6 @@ class AccountSecurityImpl extends BaseServiceImplementation implements }); } - public void registerEmail(final String address, - final AsyncCallback cb) { - if (authConfig.getAuthType() == AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT) { - try { - accountManager.link(user.get().getAccountId(), - AuthRequest.forEmail(address)); - cb.onSuccess(user.get().getAccount()); - } catch (AccountException e) { - cb.onFailure(e); - } - } else { - try { - final RegisterNewEmailSender sender; - sender = registerNewEmailFactory.create(address); - sender.send(); - } catch (EmailException e) { - log.error("Cannot send email verification message to " + address, e); - cb.onFailure(e); - } catch (RuntimeException e) { - log.error("Cannot send email verification message to " + address, e); - cb.onFailure(e); - } - } - } - public void validateEmail(final String tokenString, final AsyncCallback callback) { try { @@ -342,6 +303,8 @@ class AccountSecurityImpl extends BaseServiceImplementation implements callback.onFailure(e); } catch (AccountException e) { callback.onFailure(e); + } catch (OrmException e) { + callback.onFailure(e); } } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java index 18274469d2..c21d375949 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java @@ -386,46 +386,42 @@ public class AccountManager { * cannot be linked at this time. */ public AuthResult link(final Account.Id to, AuthRequest who) - throws AccountException { + throws AccountException, OrmException { + final ReviewDb db = schema.open(); try { - final ReviewDb db = schema.open(); - try { - who = realm.link(db, to, who); + who = realm.link(db, to, who); - final AccountExternalId.Key key = id(who); - AccountExternalId extId = db.accountExternalIds().get(key); - if (extId != null) { - if (!extId.getAccountId().equals(to)) { - throw new AccountException("Identity in use by another account"); - } - update(db, who, extId); + final AccountExternalId.Key key = id(who); + AccountExternalId extId = db.accountExternalIds().get(key); + if (extId != null) { + if (!extId.getAccountId().equals(to)) { + throw new AccountException("Identity in use by another account"); + } + update(db, who, extId); - } else { - extId = createId(to, who); - extId.setEmailAddress(who.getEmailAddress()); - db.accountExternalIds().insert(Collections.singleton(extId)); + } else { + extId = createId(to, who); + extId.setEmailAddress(who.getEmailAddress()); + db.accountExternalIds().insert(Collections.singleton(extId)); - if (who.getEmailAddress() != null) { - final Account a = db.accounts().get(to); - if (a.getPreferredEmail() == null) { - a.setPreferredEmail(who.getEmailAddress()); - db.accounts().update(Collections.singleton(a)); - } - } - - if (who.getEmailAddress() != null) { - byEmailCache.evict(who.getEmailAddress()); - byIdCache.evict(to); + if (who.getEmailAddress() != null) { + final Account a = db.accounts().get(to); + if (a.getPreferredEmail() == null) { + a.setPreferredEmail(who.getEmailAddress()); + db.accounts().update(Collections.singleton(a)); } } - return new AuthResult(to, key, false); - - } finally { - db.close(); + if (who.getEmailAddress() != null) { + byEmailCache.evict(who.getEmailAddress()); + byIdCache.evict(to); + } } - } catch (OrmException e) { - throw new AccountException("Cannot link identity", e); + + return new AuthResult(to, key, false); + + } finally { + db.close(); } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java new file mode 100644 index 0000000000..a2c71da49c --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java @@ -0,0 +1,96 @@ +// Copyright (C) 2013 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.common.errors.EmailException; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.reviewdb.client.AuthType; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.account.CreateEmail.Input; +import com.google.gerrit.server.account.GetEmails.EmailInfo; +import com.google.gerrit.server.config.AuthConfig; +import com.google.gerrit.server.mail.RegisterNewEmailSender; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.assistedinject.Assisted; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CreateEmail implements RestModifyView { + private final Logger log = LoggerFactory.getLogger(getClass()); + + static class Input { + } + + static interface Factory { + CreateEmail create(String email); + } + + private final Provider self; + private final AuthConfig authConfig; + private final AccountManager accountManager; + private final RegisterNewEmailSender.Factory registerNewEmailFactory; + private final String email; + + @Inject + CreateEmail(Provider self, AuthConfig authConfig, + AccountManager accountManager, + RegisterNewEmailSender.Factory registerNewEmailFactory, + @Assisted String email) { + this.self = self; + this.authConfig = authConfig; + this.accountManager = accountManager; + this.registerNewEmailFactory = registerNewEmailFactory; + this.email = email; + } + + @Override + public Object apply(AccountResource rsrc, Input input) throws AuthException, + ResourceConflictException, OrmException, EmailException { + IdentifiedUser s = (IdentifiedUser) self.get(); + if (s.getAccountId().get() != rsrc.getUser().getAccountId().get() + && !self.get().getCapabilities().canAdministrateServer()) { + throw new AuthException("not allowed to add email address"); + } + if (authConfig.getAuthType() == AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT) { + try { + accountManager.link(rsrc.getUser().getAccountId(), + AuthRequest.forEmail(email)); + } catch (AccountException e) { + throw new ResourceConflictException(e.getMessage()); + } + } else { + try { + RegisterNewEmailSender sender = registerNewEmailFactory.create(email); + sender.send(); + } catch (EmailException e) { + log.error("Cannot send email verification message to " + email, e); + throw e; + } catch (RuntimeException e) { + log.error("Cannot send email verification message to " + email, e); + throw e; + } + } + EmailInfo e = new EmailInfo(); + e.email = email; + return Response.created(e); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java index 59f04ac406..9a5de2ca4f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java @@ -16,6 +16,7 @@ package com.google.gerrit.server.account; import com.google.common.base.Strings; import com.google.gerrit.extensions.registration.DynamicMap; +import com.google.gerrit.extensions.restapi.AcceptsCreate; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.ChildCollection; import com.google.gerrit.extensions.restapi.IdString; @@ -29,20 +30,23 @@ import com.google.inject.Inject; import com.google.inject.Provider; public class Emails implements - ChildCollection { + ChildCollection, + AcceptsCreate { private final DynamicMap> views; private final Provider get; private final AccountByEmailCache byEmailCache; private final Provider self; + private final CreateEmail.Factory createEmailFactory; @Inject Emails(DynamicMap> views, Provider get, AccountByEmailCache byEmailCache, - Provider self) { + Provider self, CreateEmail.Factory createEmailFactory) { this.views = views; this.get = get; this.byEmailCache = byEmailCache; this.self = self; + this.createEmailFactory = createEmailFactory; } @Override @@ -80,4 +84,10 @@ public class Emails implements public DynamicMap> views() { return views; } + + @SuppressWarnings("unchecked") + @Override + public CreateEmail create(AccountResource parent, IdString email) { + return createEmailFactory.create(email.get()); + } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java index 40de3a6d29..860a96e31c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java @@ -39,6 +39,7 @@ public class Module extends RestApiModule { delete(ACCOUNT_KIND, "name").to(PutName.class); child(ACCOUNT_KIND, "emails").to(Emails.class); get(EMAIL_KIND).to(GetEmail.class); + put(EMAIL_KIND).to(PutEmail.class); put(EMAIL_KIND, "preferred").to(PutPreferred.class); get(ACCOUNT_KIND, "avatar").to(GetAvatar.class); get(ACCOUNT_KIND, "avatar.change.url").to(GetAvatarChangeUrl.class); @@ -49,5 +50,6 @@ public class Module extends RestApiModule { get(CAPABILITY_KIND).to(GetCapabilities.CheckOne.class); install(new FactoryModuleBuilder().build(CreateAccount.Factory.class)); + install(new FactoryModuleBuilder().build(CreateEmail.Factory.class)); } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutEmail.java new file mode 100644 index 0000000000..d774897601 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutEmail.java @@ -0,0 +1,29 @@ +// Copyright (C) 2013 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.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.server.account.CreateEmail.Input; + +public class PutEmail implements RestModifyView { + + @Override + public Object apply(AccountResource.Email rsrc, Input input) + throws ResourceConflictException { + throw new ResourceConflictException("Email \"" + rsrc.getEmail() + + "\" already exists"); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java index a7574c9b9e..2b8521b1bf 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java @@ -89,6 +89,7 @@ import com.google.gerrit.server.mail.FromAddressGeneratorProvider; import com.google.gerrit.server.mail.MergeFailSender; import com.google.gerrit.server.mail.MergedSender; import com.google.gerrit.server.mail.RebasedPatchSetSender; +import com.google.gerrit.server.mail.RegisterNewEmailSender; import com.google.gerrit.server.mail.ReplacePatchSetSender; import com.google.gerrit.server.mail.VelocityRuntimeProvider; import com.google.gerrit.server.patch.PatchListCacheImpl; @@ -194,6 +195,7 @@ public class GerritGlobalModule extends FactoryModule { factory(ProjectNode.Factory.class); factory(ProjectState.Factory.class); factory(RebasedPatchSetSender.Factory.class); + factory(RegisterNewEmailSender.Factory.class); factory(ReplacePatchSetSender.Factory.class); factory(PerformCreateProject.Factory.class); factory(GarbageCollection.Factory.class); diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java index a3e9d6ef23..4c23833b68 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java @@ -269,6 +269,8 @@ final class SetAccountCommand extends BaseCommand { manager.link(id, AuthRequest.forEmail(mailAddress)); } catch (AccountException ex) { throw die(ex.getMessage()); + } catch (OrmException ex) { + throw die(ex.getMessage()); } }