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 Footer templates will determine the contents of the footer text appended to
the end of all outgoing emails after the ChangeFooter and CommentFooter. 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 === Merged.soy and MergedHtml.soy
The Merged templates will determine the contents of the email related to a 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 @Test
public void userCanGenerateNewHttpPassword() throws Exception { public void userCanGenerateNewHttpPassword() throws Exception {
sender.clear();
String newPassword = gApi.accounts().self().generateHttpPassword(); String newPassword = gApi.accounts().self().generateHttpPassword();
assertThat(newPassword).isNotNull(); assertThat(newPassword).isNotNull();
assertThat(sender.getMessages()).hasSize(1);
assertThat(sender.getMessages().get(0).body()).contains("HTTP password was added or updated");
} }
@Test @Test
public void adminCanGenerateNewHttpPasswordForUser() throws Exception { public void adminCanGenerateNewHttpPasswordForUser() throws Exception {
setApiUser(admin); setApiUser(admin);
sender.clear();
String newPassword = gApi.accounts().id(user.username).generateHttpPassword(); String newPassword = gApi.accounts().id(user.username).generateHttpPassword();
assertThat(newPassword).isNotNull(); assertThat(newPassword).isNotNull();
assertThat(sender.getMessages()).hasSize(1);
assertThat(sender.getMessages().get(0).body()).contains("HTTP password was added or updated");
} }
@Test @Test
@@ -1939,7 +1945,10 @@ public class AccountIT extends AbstractDaemonTest {
@Test @Test
public void userCanRemoveHttpPassword() throws Exception { public void userCanRemoveHttpPassword() throws Exception {
setApiUser(user); setApiUser(user);
sender.clear();
assertThat(gApi.accounts().self().setHttpPassword(null)).isNull(); assertThat(gApi.accounts().self().setHttpPassword(null)).isNull();
assertThat(sender.getMessages()).hasSize(1);
assertThat(sender.getMessages().get(0).body()).contains("HTTP password was deleted");
} }
@Test @Test
@@ -1953,14 +1962,20 @@ public class AccountIT extends AbstractDaemonTest {
public void adminCanExplicitlySetHttpPasswordForUser() throws Exception { public void adminCanExplicitlySetHttpPasswordForUser() throws Exception {
setApiUser(admin); setApiUser(admin);
String httpPassword = "new-password-for-user"; String httpPassword = "new-password-for-user";
sender.clear();
assertThat(gApi.accounts().id(user.username).setHttpPassword(httpPassword)) assertThat(gApi.accounts().id(user.username).setHttpPassword(httpPassword))
.isEqualTo(httpPassword); .isEqualTo(httpPassword);
assertThat(sender.getMessages()).hasSize(1);
assertThat(sender.getMessages().get(0).body()).contains("HTTP password was added or updated");
} }
@Test @Test
public void adminCanRemoveHttpPasswordForUser() throws Exception { public void adminCanRemoveHttpPasswordForUser() throws Exception {
setApiUser(admin); setApiUser(admin);
sender.clear();
assertThat(gApi.accounts().id(user.username).setHttpPassword(null)).isNull(); 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 @Test

View File

@@ -124,6 +124,8 @@ public class SitePathInitializer {
extractMailExample("Footer.soy"); extractMailExample("Footer.soy");
extractMailExample("FooterHtml.soy"); extractMailExample("FooterHtml.soy");
extractMailExample("HeaderHtml.soy"); extractMailExample("HeaderHtml.soy");
extractMailExample("HttpPasswordUpdate.soy");
extractMailExample("HttpPasswordUpdateHtml.soy");
extractMailExample("Merged.soy"); extractMailExample("Merged.soy");
extractMailExample("MergedHtml.soy"); extractMailExample("MergedHtml.soy");
extractMailExample("NewChange.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 static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
import com.google.common.base.Strings; 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.AuthException;
import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException; 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.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIds; import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.account.externalids.ExternalIdsUpdate; 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.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -39,8 +41,12 @@ import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Base64;
import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.ConfigInvalidException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PutHttpPassword implements RestModifyView<AccountResource, Input> { public class PutHttpPassword implements RestModifyView<AccountResource, Input> {
private static final Logger log = LoggerFactory.getLogger(PutHttpPassword.class);
public static class Input { public static class Input {
public String httpPassword; public String httpPassword;
public boolean generate; public boolean generate;
@@ -61,17 +67,20 @@ public class PutHttpPassword implements RestModifyView<AccountResource, Input> {
private final PermissionBackend permissionBackend; private final PermissionBackend permissionBackend;
private final ExternalIds externalIds; private final ExternalIds externalIds;
private final ExternalIdsUpdate.User externalIdsUpdate; private final ExternalIdsUpdate.User externalIdsUpdate;
private final HttpPasswordUpdateSender.Factory httpPasswordUpdateSenderFactory;
@Inject @Inject
PutHttpPassword( PutHttpPassword(
Provider<CurrentUser> self, Provider<CurrentUser> self,
PermissionBackend permissionBackend, PermissionBackend permissionBackend,
ExternalIds externalIds, ExternalIds externalIds,
ExternalIdsUpdate.User externalIdsUpdate) { ExternalIdsUpdate.User externalIdsUpdate,
HttpPasswordUpdateSender.Factory httpPasswordUpdateSenderFactory) {
this.self = self; this.self = self;
this.permissionBackend = permissionBackend; this.permissionBackend = permissionBackend;
this.externalIds = externalIds; this.externalIds = externalIds;
this.externalIdsUpdate = externalIdsUpdate; this.externalIdsUpdate = externalIdsUpdate;
this.httpPasswordUpdateSenderFactory = httpPasswordUpdateSenderFactory;
} }
@Override @Override
@@ -111,6 +120,17 @@ public class PutHttpPassword implements RestModifyView<AccountResource, Input> {
ExternalId.createWithPassword(extId.key(), extId.accountId(), extId.email(), newPassword); ExternalId.createWithPassword(extId.key(), extId.accountId(), extId.email(), newPassword);
externalIdsUpdate.create().upsert(newExtId); 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); 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.DeleteKeySender;
import com.google.gerrit.server.mail.send.DeleteReviewerSender; import com.google.gerrit.server.mail.send.DeleteReviewerSender;
import com.google.gerrit.server.mail.send.DeleteVoteSender; 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.MergedSender;
import com.google.gerrit.server.mail.send.RegisterNewEmailSender; import com.google.gerrit.server.mail.send.RegisterNewEmailSender;
import com.google.gerrit.server.mail.send.ReplacePatchSetSender; import com.google.gerrit.server.mail.send.ReplacePatchSetSender;
@@ -41,6 +42,7 @@ public class EmailModule extends FactoryModule {
factory(DeleteKeySender.Factory.class); factory(DeleteKeySender.Factory.class);
factory(DeleteReviewerSender.Factory.class); factory(DeleteReviewerSender.Factory.class);
factory(DeleteVoteSender.Factory.class); factory(DeleteVoteSender.Factory.class);
factory(HttpPasswordUpdateSender.Factory.class);
factory(MergedSender.Factory.class); factory(MergedSender.Factory.class);
factory(RegisterNewEmailSender.Factory.class); factory(RegisterNewEmailSender.Factory.class);
factory(ReplacePatchSetSender.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", "Footer.soy",
"FooterHtml.soy", "FooterHtml.soy",
"HeaderHtml.soy", "HeaderHtml.soy",
"HttpPasswordUpdate.soy",
"HttpPasswordUpdateHtml.soy",
"Merged.soy", "Merged.soy",
"MergedHtml.soy", "MergedHtml.soy",
"NewChange.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}