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>
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 9ba8264..c641f94 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -126,6 +126,32 @@
   }
 ----
 
+[[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 @@
 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
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java
new file mode 100644
index 0000000..9d8320ad
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java
@@ -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);
+  }
+}
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 0a1f454..0ca0207 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
@@ -47,8 +47,4 @@
   @SignInRequired
   void enterAgreement(String agreementName,
       AsyncCallback<VoidResult> callback);
-
-  @Audit
-  @SignInRequired
-  void validateEmail(String token, AsyncCallback<VoidResult> callback);
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ValidateEmailScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ValidateEmailScreen.java
index 1164efc..b79723b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ValidateEmailScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ValidateEmailScreen.java
@@ -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 @@
   @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) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ConfigServerApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ConfigServerApi.java
index ae5cad2..6e65ccd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ConfigServerApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ConfigServerApi.java
@@ -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 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() {
+    }
+  }
 }
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 ce235c4..55586b0 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
@@ -34,12 +34,9 @@
 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 @@
   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 @@
   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 @@
     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 @@
       }
     });
   }
-
-  @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);
-    }
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfirmEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfirmEmail.java
new file mode 100644
index 0000000..789af9d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfirmEmail.java
@@ -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");
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
index 31fdc1e..e909f17 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
@@ -38,5 +38,6 @@
     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);
   }
 }