Send an email notification when the HTTP password is deleted or changed

We already send a notification when an account's SSH/GPG keys are added
or removed. Also send a notification when the HTTP password is changed
or deleted. This will alert the user if their account is compromised
and the HTTP password is altered by an attacker.

Also-by: David Pursehouse <dpursehouse@collab.net>
Change-Id: Iaf0e7900c98f6e29b5d609fc7d43797e7e76d1ec
diff --git a/Documentation/config-mail.txt b/Documentation/config-mail.txt
index db5228d..91c7abb 100644
--- a/Documentation/config-mail.txt
+++ b/Documentation/config-mail.txt
@@ -88,6 +88,11 @@
 The Footer templates will determine the contents of the footer text appended to
 the end of all outgoing emails after the ChangeFooter and CommentFooter.
 
+=== HttpPasswordUpdate.soy and HttpPasswordUpdateHtml.soy
+
+HttpPasswordUpdate templates will determine the contents of the email related to adding,
+changing or deleting the HTTP password on a user account.
+
 === Merged.soy and MergedHtml.soy
 
 The Merged templates will determine the contents of the email related to a
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index ec96c4b..1e1fc11 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -1904,15 +1904,21 @@
 
   @Test
   public void userCanGenerateNewHttpPassword() throws Exception {
+    sender.clear();
     String newPassword = gApi.accounts().self().generateHttpPassword();
     assertThat(newPassword).isNotNull();
+    assertThat(sender.getMessages()).hasSize(1);
+    assertThat(sender.getMessages().get(0).body()).contains("HTTP password was added or updated");
   }
 
   @Test
   public void adminCanGenerateNewHttpPasswordForUser() throws Exception {
     setApiUser(admin);
+    sender.clear();
     String newPassword = gApi.accounts().id(user.username).generateHttpPassword();
     assertThat(newPassword).isNotNull();
+    assertThat(sender.getMessages()).hasSize(1);
+    assertThat(sender.getMessages().get(0).body()).contains("HTTP password was added or updated");
   }
 
   @Test
@@ -1939,7 +1945,10 @@
   @Test
   public void userCanRemoveHttpPassword() throws Exception {
     setApiUser(user);
+    sender.clear();
     assertThat(gApi.accounts().self().setHttpPassword(null)).isNull();
+    assertThat(sender.getMessages()).hasSize(1);
+    assertThat(sender.getMessages().get(0).body()).contains("HTTP password was deleted");
   }
 
   @Test
@@ -1953,14 +1962,20 @@
   public void adminCanExplicitlySetHttpPasswordForUser() throws Exception {
     setApiUser(admin);
     String httpPassword = "new-password-for-user";
+    sender.clear();
     assertThat(gApi.accounts().id(user.username).setHttpPassword(httpPassword))
         .isEqualTo(httpPassword);
+    assertThat(sender.getMessages()).hasSize(1);
+    assertThat(sender.getMessages().get(0).body()).contains("HTTP password was added or updated");
   }
 
   @Test
   public void adminCanRemoveHttpPasswordForUser() throws Exception {
     setApiUser(admin);
+    sender.clear();
     assertThat(gApi.accounts().id(user.username).setHttpPassword(null)).isNull();
+    assertThat(sender.getMessages()).hasSize(1);
+    assertThat(sender.getMessages().get(0).body()).contains("HTTP password was deleted");
   }
 
   @Test
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
index a633335..86daae2 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
@@ -124,6 +124,8 @@
     extractMailExample("Footer.soy");
     extractMailExample("FooterHtml.soy");
     extractMailExample("HeaderHtml.soy");
+    extractMailExample("HttpPasswordUpdate.soy");
+    extractMailExample("HttpPasswordUpdateHtml.soy");
     extractMailExample("Merged.soy");
     extractMailExample("MergedHtml.soy");
     extractMailExample("NewChange.soy");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
index ef953e6..19b5a66 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
 
 import com.google.common.base.Strings;
+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.ResourceNotFoundException;
@@ -28,6 +29,7 @@
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
+import com.google.gerrit.server.mail.send.HttpPasswordUpdateSender;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -39,8 +41,12 @@
 import java.security.SecureRandom;
 import org.apache.commons.codec.binary.Base64;
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 public class PutHttpPassword implements RestModifyView<AccountResource, Input> {
+  private static final Logger log = LoggerFactory.getLogger(PutHttpPassword.class);
+
   public static class Input {
     public String httpPassword;
     public boolean generate;
@@ -61,17 +67,20 @@
   private final PermissionBackend permissionBackend;
   private final ExternalIds externalIds;
   private final ExternalIdsUpdate.User externalIdsUpdate;
+  private final HttpPasswordUpdateSender.Factory httpPasswordUpdateSenderFactory;
 
   @Inject
   PutHttpPassword(
       Provider<CurrentUser> self,
       PermissionBackend permissionBackend,
       ExternalIds externalIds,
-      ExternalIdsUpdate.User externalIdsUpdate) {
+      ExternalIdsUpdate.User externalIdsUpdate,
+      HttpPasswordUpdateSender.Factory httpPasswordUpdateSenderFactory) {
     this.self = self;
     this.permissionBackend = permissionBackend;
     this.externalIds = externalIds;
     this.externalIdsUpdate = externalIdsUpdate;
+    this.httpPasswordUpdateSenderFactory = httpPasswordUpdateSenderFactory;
   }
 
   @Override
@@ -111,6 +120,17 @@
         ExternalId.createWithPassword(extId.key(), extId.accountId(), extId.email(), newPassword);
     externalIdsUpdate.create().upsert(newExtId);
 
+    try {
+      httpPasswordUpdateSenderFactory
+          .create(user, newPassword == null ? "deleted" : "added or updated")
+          .send();
+    } catch (EmailException e) {
+      log.error(
+          "Cannot send HttpPassword update message to {}",
+          user.getAccount().getPreferredEmail(),
+          e);
+    }
+
     return Strings.isNullOrEmpty(newPassword) ? Response.<String>none() : Response.ok(newPassword);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailModule.java
index 6b363c0..cc3db75 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailModule.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.server.mail.send.DeleteKeySender;
 import com.google.gerrit.server.mail.send.DeleteReviewerSender;
 import com.google.gerrit.server.mail.send.DeleteVoteSender;
+import com.google.gerrit.server.mail.send.HttpPasswordUpdateSender;
 import com.google.gerrit.server.mail.send.MergedSender;
 import com.google.gerrit.server.mail.send.RegisterNewEmailSender;
 import com.google.gerrit.server.mail.send.ReplacePatchSetSender;
@@ -41,6 +42,7 @@
     factory(DeleteKeySender.Factory.class);
     factory(DeleteReviewerSender.Factory.class);
     factory(DeleteVoteSender.Factory.class);
+    factory(HttpPasswordUpdateSender.Factory.class);
     factory(MergedSender.Factory.class);
     factory(RegisterNewEmailSender.Factory.class);
     factory(ReplacePatchSetSender.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
new file mode 100644
index 0000000..eb2ca25
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2019 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.mail.send;
+
+import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.mail.Address;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+public class HttpPasswordUpdateSender extends OutgoingEmail {
+  public interface Factory {
+    HttpPasswordUpdateSender create(IdentifiedUser user, String operation);
+  }
+
+  private final IdentifiedUser user;
+  private final String operation;
+
+  @AssistedInject
+  public HttpPasswordUpdateSender(
+      EmailArguments ea, @Assisted IdentifiedUser user, @Assisted String operation) {
+    super(ea, "HttpPasswordUpdate");
+    this.user = user;
+    this.operation = operation;
+  }
+
+  @Override
+  protected void init() throws EmailException {
+    super.init();
+    setHeader("Subject", "[Gerrit Code Review] HTTP password was " + operation);
+    add(RecipientType.TO, new Address(getEmail()));
+  }
+
+  @Override
+  protected boolean shouldSendMessage() {
+    // Always send an email if the HTTP password is updated.
+    return true;
+  }
+
+  @Override
+  protected void format() throws EmailException {
+    appendText(textTemplate("HttpPasswordUpdate"));
+    if (useHtml()) {
+      appendHtml(soyHtmlTemplate("HttpPasswordUpdateHtml"));
+    }
+  }
+
+  public String getEmail() {
+    return user.getAccount().getPreferredEmail();
+  }
+
+  public String getUserNameEmail() {
+    return getUserNameEmailFor(user.getAccountId());
+  }
+
+  @Override
+  protected void setupSoyContext() {
+    super.setupSoyContext();
+    soyContextEmailData.put("email", getEmail());
+    soyContextEmailData.put("userNameEmail", getUserNameEmail());
+    soyContextEmailData.put("operation", operation);
+  }
+
+  @Override
+  protected boolean supportsHtml() {
+    return true;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/MailSoyTofuProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/MailSoyTofuProvider.java
index 81074fd..34a7085 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/MailSoyTofuProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/MailSoyTofuProvider.java
@@ -56,6 +56,8 @@
     "Footer.soy",
     "FooterHtml.soy",
     "HeaderHtml.soy",
+    "HttpPasswordUpdate.soy",
+    "HttpPasswordUpdateHtml.soy",
     "Merged.soy",
     "MergedHtml.soy",
     "NewChange.soy",
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/HttpPasswordUpdate.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/HttpPasswordUpdate.soy
new file mode 100644
index 0000000..49fce7b
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/HttpPasswordUpdate.soy
@@ -0,0 +1,55 @@
+/**
+ * Copyright (C) 2019 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.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+/**
+ * The .HttpPasswordUpdate template will determine the contents of the email related to
+ * adding, changing or deleting the HTTP password.
+ * @param email
+ */
+{template .HttpPasswordUpdate autoescape="strict" kind="text"}
+  The HTTP password was {$email.operation} on Gerrit Code Review at
+  {sp}{$email.gerritHost}.
+
+  If this is not expected, please contact your Gerrit Administrators
+  immediately.
+
+  {\n}
+  {\n}
+
+  You can also manage your HTTP password by visiting
+  {\n}
+  {$email.gerritUrl}#/settings/http-password
+  {\n}
+  {if $email.userNameEmail}
+    (while signed in as {$email.userNameEmail})
+  {else}
+    (while signed in as {$email.email})
+  {/if}
+
+  {\n}
+  {\n}
+
+  If clicking the link above does not work, copy and paste the URL in a new
+  browser window instead.
+
+  {\n}
+  {\n}
+
+  This is a send-only email address.  Replies to this message will not be read
+  or answered.
+{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/HttpPasswordUpdateHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/HttpPasswordUpdateHtml.soy
new file mode 100644
index 0000000..0aac668
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/HttpPasswordUpdateHtml.soy
@@ -0,0 +1,48 @@
+/**
+ * Copyright (C) 2019 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.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+/**
+ * @param email
+ */
+{template .HttpPasswordUpdateHtml autoescape="strict" kind="html"}
+  <p>
+    The HTTP password was {$email.operation} on Gerrit Code Review
+    at {$email.gerritHost}.
+  </p>
+
+  <p>
+    If this is not expected, please contact your Gerrit Administrators
+    immediately.
+  </p>
+
+  <p>
+    You can also manage your HTTP password by following{sp}
+    <a href="{$email.gerritUrl}#/settings/http-password">this link</a>
+    {sp}
+    {if $email.userNameEmail}
+      (while signed in as {$email.userNameEmail})
+    {else}
+      (while signed in as {$email.email})
+    {/if}.
+  </p>
+
+  <p>
+    This is a send-only email address.  Replies to this message will not be read
+    or answered.
+  </p>
+{/template}