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
This commit is contained in:
Paladox none
2019-04-22 14:39:33 +00:00
committed by David Pursehouse
parent 04a1704113
commit d189897aaa
9 changed files with 231 additions and 1 deletions

View File

@@ -88,6 +88,11 @@ a user removing a reviewer (with a vote) from a change. It is a
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

View File

@@ -1904,15 +1904,21 @@ public class AccountIT extends AbstractDaemonTest {
@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 @@ public class AccountIT extends AbstractDaemonTest {
@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 class AccountIT extends AbstractDaemonTest {
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

View File

@@ -124,6 +124,8 @@ public class SitePathInitializer {
extractMailExample("Footer.soy");
extractMailExample("FooterHtml.soy");
extractMailExample("HeaderHtml.soy");
extractMailExample("HttpPasswordUpdate.soy");
extractMailExample("HttpPasswordUpdateHtml.soy");
extractMailExample("Merged.soy");
extractMailExample("MergedHtml.soy");
extractMailExample("NewChange.soy");

View File

@@ -17,6 +17,7 @@ package com.google.gerrit.server.account;
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.PutHttpPassword.Input;
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.NoSuchAlgorithmException;
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 @@ public class PutHttpPassword implements RestModifyView<AccountResource, Input> {
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 @@ public class PutHttpPassword implements RestModifyView<AccountResource, Input> {
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);
}

View File

@@ -23,6 +23,7 @@ import com.google.gerrit.server.mail.send.CreateChangeSender;
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 @@ public class EmailModule extends FactoryModule {
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);

View File

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

View File

@@ -56,6 +56,8 @@ public class MailSoyTofuProvider implements Provider<SoyTofu> {
"Footer.soy",
"FooterHtml.soy",
"HeaderHtml.soy",
"HttpPasswordUpdate.soy",
"HttpPasswordUpdateHtml.soy",
"Merged.soy",
"MergedHtml.soy",
"NewChange.soy",

View File

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

View File

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