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:

committed by
David Pursehouse

parent
04a1704113
commit
d189897aaa
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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");
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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);
|
||||||
|
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
@@ -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",
|
||||||
|
@@ -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}
|
@@ -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}
|
Reference in New Issue
Block a user