Merge "Support to register a new email address via REST"

This commit is contained in:
David Pursehouse 2013-05-23 06:39:02 +00:00 committed by Gerrit Code Review
commit d46269a288
13 changed files with 249 additions and 85 deletions

View File

@ -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
~~~~~~~~~~~~~~~~~~~

View File

@ -75,10 +75,6 @@ public interface AccountSecurity extends RemoteJsonService {
void enterAgreement(String agreementName,
AsyncCallback<VoidResult> callback);
@Audit
@SignInRequired
void registerEmail(String address, AsyncCallback<Account> callback);
@Audit
@SignInRequired
void validateEmail(String token, AsyncCallback<VoidResult> callback);

View File

@ -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<NativeString> cb) {
JavaScriptObject in = JavaScriptObject.createObject();
new RestApi("/accounts/").id(account).view("emails").id(email)
.ifNoneMatch().put(in, cb);
}
}

View File

@ -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<Account>() {
public void onSuccess(Account currentUser) {
AccountApi.registerEmail("self", addr, new GerritCallback<NativeString>() {
@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);
}

View File

@ -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);

View File

@ -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<IdentifiedUser> 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<ReviewDb> schema,
final Provider<CurrentUser> currentUser, final ContactStore cs,
final AuthConfig ac, final Realm r, final Provider<IdentifiedUser> u,
final Realm r, final Provider<IdentifiedUser> 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<Account> 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<VoidResult> 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);
}
}
}

View File

@ -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();
}
}

View File

@ -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<AccountResource, Input> {
private final Logger log = LoggerFactory.getLogger(getClass());
static class Input {
}
static interface Factory {
CreateEmail create(String email);
}
private final Provider<CurrentUser> self;
private final AuthConfig authConfig;
private final AccountManager accountManager;
private final RegisterNewEmailSender.Factory registerNewEmailFactory;
private final String email;
@Inject
CreateEmail(Provider<CurrentUser> 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);
}
}

View File

@ -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<AccountResource, AccountResource.Email> {
ChildCollection<AccountResource, AccountResource.Email>,
AcceptsCreate<AccountResource> {
private final DynamicMap<RestView<AccountResource.Email>> views;
private final Provider<GetEmails> get;
private final AccountByEmailCache byEmailCache;
private final Provider<CurrentUser> self;
private final CreateEmail.Factory createEmailFactory;
@Inject
Emails(DynamicMap<RestView<AccountResource.Email>> views,
Provider<GetEmails> get, AccountByEmailCache byEmailCache,
Provider<CurrentUser> self) {
Provider<CurrentUser> 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<RestView<Email>> views() {
return views;
}
@SuppressWarnings("unchecked")
@Override
public CreateEmail create(AccountResource parent, IdString email) {
return createEmailFactory.create(email.get());
}
}

View File

@ -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));
}
}

View File

@ -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<AccountResource.Email, Input> {
@Override
public Object apply(AccountResource.Email rsrc, Input input)
throws ResourceConflictException {
throw new ResourceConflictException("Email \"" + rsrc.getEmail()
+ "\" already exists");
}
}

View File

@ -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);

View File

@ -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());
}
}