Add REST endpoint to confirm emails

Use the new REST endpoint in the UI instead of the old
AccountSecurity.validateEmail(...) RPC.

AccountSecurity.validateEmail(...) is removed since it is no longer
used.

Change-Id: I561224e9d9ea31875df2bba838ee53f77f24c55b
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
This commit is contained in:
Edwin Kempin
2015-07-24 14:17:11 +02:00
parent f07e98ba5c
commit ed84657c8e
8 changed files with 208 additions and 33 deletions

View File

@@ -126,6 +126,32 @@ As result a link:#server-info[ServerInfo] entity is returned.
}
----
[[confirm-email]]
=== Confirm Email
--
'PUT /config/server/email.confirm'
--
Confirms that the user owns an email address.
The email token must be provided in the request body inside
an link:#email-confirmation-input[EmailConfirmationInput] entity.
.Request
----
PUT /config/server/email.confirm HTTP/1.0
Content-Type: application/json; charset=UTF-8
{
"token": "Enim+QNbAo6TV8Hur8WwoUypI6apG7qBPvF+bw==$MTAwMDAwNDp0ZXN0QHRlc3QuZGU="
}
----
The response is "`204 No Content`".
If the token is invalid or if it's the token of another user the
request fails and the response is "`422 Unprocessable Entity`".
[[list-caches]]
=== List Caches
@@ -1132,6 +1158,18 @@ Empty, if accessed anonymously and the download scheme requires
authentication.
|=================================
[[email-confirmation-input]]
=== EmailConfirmationInput
The `EmailConfirmationInput` entity contains information for confirming
an email address.
[options="header",cols="1,6"]
|=======================
|Field Name |Description
|`token` |
The token that was sent by mail to a newly registered email address.
|=======================
[[entries-info]]
=== EntriesInfo
The `EntriesInfo` entity contains information about the entries in a

View File

@@ -0,0 +1,66 @@
// Copyright (C) 2015 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.acceptance.rest.config;
import static com.google.common.truth.Truth.assertThat;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.server.config.ConfirmEmail;
import com.google.gerrit.server.mail.EmailTokenVerifier;
import com.google.gerrit.testutil.ConfigSuite;
import com.google.gwtjsonrpc.server.SignedToken;
import com.google.inject.Inject;
import org.apache.http.HttpStatus;
import org.eclipse.jgit.lib.Config;
import org.junit.Test;
public class ConfirmEmailIT extends AbstractDaemonTest {
@ConfigSuite.Default
public static Config defaultConfig() {
Config cfg = new Config();
cfg.setString("auth", null, "registerEmailPrivateKey",
SignedToken.generateRandomKey());
return cfg;
}
@Inject
private EmailTokenVerifier emailTokenVerifier;
@Test
public void confirm() throws Exception {
ConfirmEmail.Input in = new ConfirmEmail.Input();
in.token = emailTokenVerifier.encode(admin.getId(), "new.mail@example.com");
RestResponse r = adminSession.put("/config/server/email.confirm", in);
assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
}
@Test
public void confirmForOtherUser_UnprocessableEntity() throws Exception {
ConfirmEmail.Input in = new ConfirmEmail.Input();
in.token = emailTokenVerifier.encode(user.getId(), "new.mail@example.com");
RestResponse r = adminSession.put("/config/server/email.confirm", in);
assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_UNPROCESSABLE_ENTITY);
}
@Test
public void confirmInvalidToken_UnprocessableEntity() throws Exception {
ConfirmEmail.Input in = new ConfirmEmail.Input();
in.token = "invalidToken";
RestResponse r = adminSession.put("/config/server/email.confirm", in);
assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_UNPROCESSABLE_ENTITY);
}
}

View File

@@ -47,8 +47,4 @@ public interface AccountSecurity extends RemoteJsonService {
@SignInRequired
void enterAgreement(String agreementName,
AsyncCallback<VoidResult> callback);
@Audit
@SignInRequired
void validateEmail(String token, AsyncCallback<VoidResult> callback);
}

View File

@@ -15,10 +15,11 @@
package com.google.gerrit.client.account;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.VoidResult;
import com.google.gerrit.client.config.ConfigServerApi;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.client.ui.AccountScreen;
import com.google.gerrit.common.PageLinks;
import com.google.gwtjsonrpc.common.VoidResult;
public class ValidateEmailScreen extends AccountScreen {
private final String magicToken;
@@ -36,7 +37,7 @@ public class ValidateEmailScreen extends AccountScreen {
@Override
protected void onLoad() {
super.onLoad();
Util.ACCOUNT_SEC.validateEmail(magicToken,
ConfigServerApi.confirmEmail(magicToken,
new ScreenLoadCallback<VoidResult>(this) {
@Override
protected void preDisplay(final VoidResult result) {

View File

@@ -14,11 +14,13 @@
package com.google.gerrit.client.config;
import com.google.gerrit.client.VoidResult;
import com.google.gerrit.client.info.AccountPreferencesInfo;
import com.google.gerrit.client.info.ServerInfo;
import com.google.gerrit.client.info.TopMenuList;
import com.google.gerrit.client.rpc.NativeMap;
import com.google.gerrit.client.rpc.RestApi;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.user.client.rpc.AsyncCallback;
/**
@@ -42,4 +44,21 @@ public class ConfigServerApi {
public static void serverInfo(AsyncCallback<ServerInfo> cb) {
new RestApi("/config/server/info").get(cb);
}
public static void confirmEmail(String token, AsyncCallback<VoidResult> cb) {
EmailConfirmationInput input = EmailConfirmationInput.create();
input.setToken(token);
new RestApi("/config/server/email.confirm").put(input, cb);
}
private static class EmailConfirmationInput extends JavaScriptObject {
final native void setToken(String t) /*-{ this.t = t; }-*/;
static EmailConfirmationInput create() {
return createObject().cast();
}
protected EmailConfirmationInput() {
}
}
}

View File

@@ -34,12 +34,9 @@ import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
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.GroupCache;
import com.google.gerrit.server.account.Realm;
import com.google.gerrit.server.contact.ContactStore;
import com.google.gerrit.server.mail.EmailTokenVerifier;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtjsonrpc.common.VoidResult;
@@ -57,10 +54,8 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
private final Realm realm;
private final ProjectCache projectCache;
private final Provider<IdentifiedUser> user;
private final EmailTokenVerifier emailTokenVerifier;
private final AccountByEmailCache byEmailCache;
private final AccountCache accountCache;
private final AccountManager accountManager;
private final boolean useContactInfo;
private final DeleteExternalIds.Factory deleteExternalIdsFactory;
@@ -74,9 +69,8 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
AccountSecurityImpl(final Provider<ReviewDb> schema,
final Provider<CurrentUser> currentUser, final ContactStore cs,
final Realm r, final Provider<IdentifiedUser> u,
final EmailTokenVerifier etv, final ProjectCache pc,
final ProjectCache pc,
final AccountByEmailCache abec, final AccountCache uac,
final AccountManager am,
final DeleteExternalIds.Factory deleteExternalIdsFactory,
final ExternalIdDetailFactory.Factory externalIdDetailFactory,
final ChangeHooks hooks, final GroupCache groupCache,
@@ -85,11 +79,9 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
contactStore = cs;
realm = r;
user = u;
emailTokenVerifier = etv;
projectCache = pc;
byEmailCache = abec;
accountCache = uac;
accountManager = am;
this.auditService = auditService;
useContactInfo = contactStore != null && contactStore.isEnabled();
@@ -201,22 +193,4 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
}
});
}
@Override
public void validateEmail(final String tokenString,
final AsyncCallback<VoidResult> callback) {
try {
EmailTokenVerifier.ParsedToken token = emailTokenVerifier.decode(tokenString);
Account.Id currentUser = user.get().getAccountId();
if (currentUser.equals(token.getAccountId())) {
accountManager.link(currentUser, token.toAuthRequest());
callback.onSuccess(VoidResult.INSTANCE);
} else {
throw new EmailTokenVerifier.InvalidTokenException();
}
} catch (EmailTokenVerifier.InvalidTokenException | OrmException
| AccountException e) {
callback.onFailure(e);
}
}
}

View File

@@ -0,0 +1,80 @@
// Copyright (C) 2015 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.config;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.DefaultInput;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.config.ConfirmEmail.Input;
import com.google.gerrit.server.mail.EmailTokenVerifier;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@Singleton
public class ConfirmEmail implements RestModifyView<ConfigResource, Input> {
public static class Input {
@DefaultInput
public String token;
}
private final Provider<CurrentUser> self;
private final EmailTokenVerifier emailTokenVerifier;
private final AccountManager accountManager;
@Inject
public ConfirmEmail(Provider<CurrentUser> self,
EmailTokenVerifier emailTokenVerifier,
AccountManager accountManager) {
this.self = self;
this.emailTokenVerifier = emailTokenVerifier;
this.accountManager = accountManager;
}
@Override
public Response<?> apply(ConfigResource rsrc, Input input)
throws AuthException, UnprocessableEntityException, AccountException,
OrmException {
CurrentUser user = self.get();
if (!user.isIdentifiedUser()) {
throw new AuthException("Authentication required");
}
if (input == null) {
input = new Input();
}
try {
EmailTokenVerifier.ParsedToken token = emailTokenVerifier.decode(input.token);
Account.Id accId = ((IdentifiedUser)user).getAccountId();
if (accId.equals(token.getAccountId())) {
accountManager.link(accId, token.toAuthRequest());
return Response.none();
} else {
throw new UnprocessableEntityException("invalid token");
}
} catch (EmailTokenVerifier.InvalidTokenException e) {
throw new UnprocessableEntityException("invalid token");
}
}
}

View File

@@ -38,5 +38,6 @@ public class Module extends RestApiModule {
get(CONFIG_KIND, "info").to(GetServerInfo.class);
get(CONFIG_KIND, "preferences").to(GetPreferences.class);
put(CONFIG_KIND, "preferences").to(SetPreferences.class);
put(CONFIG_KIND, "email.confirm").to(ConfirmEmail.class);
}
}