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]]
=== List Caches === List Caches
@@ -1132,6 +1158,18 @@ Empty, if accessed anonymously and the download scheme requires
authentication. 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]] [[entries-info]]
=== EntriesInfo === EntriesInfo
The `EntriesInfo` entity contains information about the entries in a 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 @SignInRequired
void enterAgreement(String agreementName, void enterAgreement(String agreementName,
AsyncCallback<VoidResult> callback); AsyncCallback<VoidResult> callback);
@Audit
@SignInRequired
void validateEmail(String token, AsyncCallback<VoidResult> callback);
} }

View File

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

View File

@@ -14,11 +14,13 @@
package com.google.gerrit.client.config; 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.AccountPreferencesInfo;
import com.google.gerrit.client.info.ServerInfo; import com.google.gerrit.client.info.ServerInfo;
import com.google.gerrit.client.info.TopMenuList; import com.google.gerrit.client.info.TopMenuList;
import com.google.gerrit.client.rpc.NativeMap; import com.google.gerrit.client.rpc.NativeMap;
import com.google.gerrit.client.rpc.RestApi; import com.google.gerrit.client.rpc.RestApi;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.rpc.AsyncCallback;
/** /**
@@ -42,4 +44,21 @@ public class ConfigServerApi {
public static void serverInfo(AsyncCallback<ServerInfo> cb) { public static void serverInfo(AsyncCallback<ServerInfo> cb) {
new RestApi("/config/server/info").get(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.IdentifiedUser;
import com.google.gerrit.server.account.AccountByEmailCache; import com.google.gerrit.server.account.AccountByEmailCache;
import com.google.gerrit.server.account.AccountCache; 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.GroupCache;
import com.google.gerrit.server.account.Realm; import com.google.gerrit.server.account.Realm;
import com.google.gerrit.server.contact.ContactStore; import com.google.gerrit.server.contact.ContactStore;
import com.google.gerrit.server.mail.EmailTokenVerifier;
import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectCache;
import com.google.gwtjsonrpc.common.AsyncCallback; import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtjsonrpc.common.VoidResult; import com.google.gwtjsonrpc.common.VoidResult;
@@ -57,10 +54,8 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
private final Realm realm; private final Realm realm;
private final ProjectCache projectCache; private final ProjectCache projectCache;
private final Provider<IdentifiedUser> user; private final Provider<IdentifiedUser> user;
private final EmailTokenVerifier emailTokenVerifier;
private final AccountByEmailCache byEmailCache; private final AccountByEmailCache byEmailCache;
private final AccountCache accountCache; private final AccountCache accountCache;
private final AccountManager accountManager;
private final boolean useContactInfo; private final boolean useContactInfo;
private final DeleteExternalIds.Factory deleteExternalIdsFactory; private final DeleteExternalIds.Factory deleteExternalIdsFactory;
@@ -74,9 +69,8 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
AccountSecurityImpl(final Provider<ReviewDb> schema, AccountSecurityImpl(final Provider<ReviewDb> schema,
final Provider<CurrentUser> currentUser, final ContactStore cs, final Provider<CurrentUser> currentUser, final ContactStore cs,
final Realm r, final Provider<IdentifiedUser> u, final Realm r, final Provider<IdentifiedUser> u,
final EmailTokenVerifier etv, final ProjectCache pc, final ProjectCache pc,
final AccountByEmailCache abec, final AccountCache uac, final AccountByEmailCache abec, final AccountCache uac,
final AccountManager am,
final DeleteExternalIds.Factory deleteExternalIdsFactory, final DeleteExternalIds.Factory deleteExternalIdsFactory,
final ExternalIdDetailFactory.Factory externalIdDetailFactory, final ExternalIdDetailFactory.Factory externalIdDetailFactory,
final ChangeHooks hooks, final GroupCache groupCache, final ChangeHooks hooks, final GroupCache groupCache,
@@ -85,11 +79,9 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
contactStore = cs; contactStore = cs;
realm = r; realm = r;
user = u; user = u;
emailTokenVerifier = etv;
projectCache = pc; projectCache = pc;
byEmailCache = abec; byEmailCache = abec;
accountCache = uac; accountCache = uac;
accountManager = am;
this.auditService = auditService; this.auditService = auditService;
useContactInfo = contactStore != null && contactStore.isEnabled(); 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, "info").to(GetServerInfo.class);
get(CONFIG_KIND, "preferences").to(GetPreferences.class); get(CONFIG_KIND, "preferences").to(GetPreferences.class);
put(CONFIG_KIND, "preferences").to(SetPreferences.class); put(CONFIG_KIND, "preferences").to(SetPreferences.class);
put(CONFIG_KIND, "email.confirm").to(ConfirmEmail.class);
} }
} }