Add notification when adding keys

When adding SSH/GPG keys, add a notification to the user to inform
them as an additional protection for their account should any
credentials be compromised.

Change-Id: Ia448af8da33dc0be3ba1acbb354ff3628630fe09
This commit is contained in:
Doug Kelly 2015-08-25 11:02:04 -05:00 committed by David Pursehouse
parent 7539527136
commit 251b1574d2
9 changed files with 252 additions and 18 deletions

View File

@ -28,6 +28,12 @@ The `Abandoned.vm` template will determine the contents of the email related
to a change being abandoned. It is a `ChangeEmail`: see `ChangeSubject.vm` and
`ChangeFooter.vm`.
=== AddKey.vm
The `AddKey.vm` template will determine the contents of the email related to
SSH and GPG keys being added to a user account. This notification is not sent
when the key is administratively added to another user account.
=== ChangeFooter.vm
The `ChangeFooter.vm` template will determine the contents of the footer

View File

@ -27,6 +27,7 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.io.BaseEncoding;
import com.google.gerrit.common.errors.EmailException;
import com.google.gerrit.extensions.common.GpgKeyInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
@ -41,6 +42,7 @@ import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.account.AccountResource;
import com.google.gerrit.server.mail.AddKeySender;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
@ -54,6 +56,8 @@ import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@ -71,20 +75,24 @@ public class PostGpgKeys implements RestModifyView<AccountResource, Input> {
public List<String> delete;
}
private final Logger log = LoggerFactory.getLogger(getClass());
private final Provider<PersonIdent> serverIdent;
private final Provider<ReviewDb> db;
private final Provider<PublicKeyStore> storeProvider;
private final PublicKeyChecker checker;
private final AddKeySender.Factory addKeyFactory;
@Inject
PostGpgKeys(@GerritPersonIdent Provider<PersonIdent> serverIdent,
Provider<ReviewDb> db,
Provider<PublicKeyStore> storeProvider,
PublicKeyChecker checker) {
PublicKeyChecker checker,
AddKeySender.Factory addKeyFactory) {
this.serverIdent = serverIdent;
this.db = db;
this.storeProvider = storeProvider;
this.checker = checker;
this.addKeyFactory = addKeyFactory;
}
@Override
@ -180,6 +188,7 @@ public class PostGpgKeys implements RestModifyView<AccountResource, Input> {
Set<Fingerprint> toRemove) throws BadRequestException,
ResourceConflictException, PGPException, IOException {
try (PublicKeyStore store = storeProvider.get()) {
List<String> addedKeys = new ArrayList<>();
for (PGPPublicKeyRing keyRing : keyRings) {
PGPPublicKey key = keyRing.getPublicKey();
CheckResult result = checker.check(key);
@ -188,6 +197,7 @@ public class PostGpgKeys implements RestModifyView<AccountResource, Input> {
"Problems with public key %s:\n%s",
keyToString(key), Joiner.on('\n').join(result.getProblems())));
}
addedKeys.add(PublicKeyStore.keyToString(key));
store.add(keyRing);
}
for (Fingerprint fp : toRemove) {
@ -204,6 +214,13 @@ public class PostGpgKeys implements RestModifyView<AccountResource, Input> {
case NEW:
case FAST_FORWARD:
case FORCED:
try {
addKeyFactory.create(rsrc.getUser(), addedKeys).send();
} catch (EmailException e) {
log.error("Cannot send GPG key added message to "
+ rsrc.getUser().getAccount().getPreferredEmail(), e);
}
break;
case NO_CHANGE:
break;
default:

View File

@ -100,6 +100,7 @@ public class SitePathInitializer {
chmod(0700, site.tmp_dir);
extractMailExample("Abandoned.vm");
extractMailExample("AddKey.vm");
extractMailExample("ChangeFooter.vm");
extractMailExample("ChangeSubject.vm");
extractMailExample("Comment.vm");

View File

@ -18,6 +18,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.Iterables;
import com.google.common.io.ByteSource;
import com.google.gerrit.common.errors.EmailException;
import com.google.gerrit.common.errors.InvalidSshKeyException;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
@ -30,6 +31,7 @@ import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AddSshKey.Input;
import com.google.gerrit.server.account.GetSshKeys.SshKeyInfo;
import com.google.gerrit.server.mail.AddKeySender;
import com.google.gerrit.server.ssh.SshKeyCache;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
@ -37,12 +39,17 @@ import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
@Singleton
public class AddSshKey implements RestModifyView<AccountResource, Input> {
private static final Logger log = LoggerFactory.getLogger(AddSshKey.class);
public static class Input {
public RawInput raw;
}
@ -50,13 +57,15 @@ public class AddSshKey implements RestModifyView<AccountResource, Input> {
private final Provider<CurrentUser> self;
private final Provider<ReviewDb> dbProvider;
private final SshKeyCache sshKeyCache;
private final AddKeySender.Factory addKeyFactory;
@Inject
AddSshKey(Provider<CurrentUser> self, Provider<ReviewDb> dbProvider,
SshKeyCache sshKeyCache) {
SshKeyCache sshKeyCache, AddKeySender.Factory addKeyFactory) {
this.self = self;
this.dbProvider = dbProvider;
this.sshKeyCache = sshKeyCache;
this.addKeyFactory = addKeyFactory;
}
@Override
@ -96,6 +105,12 @@ public class AddSshKey implements RestModifyView<AccountResource, Input> {
sshKeyCache.create(new AccountSshKey.Id(
user.getAccountId(), max + 1), sshPublicKey);
dbProvider.get().accountSshKeys().insert(Collections.singleton(sshKey));
try {
addKeyFactory.create(user, sshKey).send();
} catch (EmailException e) {
log.error("Cannot send SSH key added message to "
+ user.getAccount().getPreferredEmail(), e);
}
sshKeyCache.evict(user.getUserName());
return Response.<SshKeyInfo>created(new SshKeyInfo(sshKey));
} catch (InvalidSshKeyException e) {

View File

@ -95,6 +95,7 @@ import com.google.gerrit.server.git.validators.UploadValidators;
import com.google.gerrit.server.group.GroupModule;
import com.google.gerrit.server.index.ReindexAfterUpdate;
import com.google.gerrit.server.mail.AddReviewerSender;
import com.google.gerrit.server.mail.AddKeySender;
import com.google.gerrit.server.mail.CreateChangeSender;
import com.google.gerrit.server.mail.EmailModule;
import com.google.gerrit.server.mail.FromAddressGenerator;
@ -186,6 +187,7 @@ public class GerritGlobalModule extends FactoryModule {
factory(AccountInfoCacheFactory.Factory.class);
factory(AddReviewerSender.Factory.class);
factory(AddKeySender.Factory.class);
factory(CapabilityControl.Factory.class);
factory(ChangeData.Factory.class);
factory(ChangeJson.Factory.class);

View File

@ -0,0 +1,113 @@
// 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.mail;
import com.google.common.base.Joiner;
import com.google.gerrit.common.errors.EmailException;
import com.google.gerrit.reviewdb.client.AccountSshKey;
import com.google.gerrit.server.IdentifiedUser;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import java.util.List;
public class AddKeySender extends OutgoingEmail {
public interface Factory {
public AddKeySender create(IdentifiedUser user, AccountSshKey sshKey);
public AddKeySender create(IdentifiedUser user, List<String> gpgKey);
}
private final IdentifiedUser callingUser;
private final IdentifiedUser user;
private final AccountSshKey sshKey;
private final List<String> gpgKeys;
@AssistedInject
public AddKeySender(EmailArguments ea,
IdentifiedUser callingUser,
@Assisted IdentifiedUser user,
@Assisted AccountSshKey sshKey) {
super(ea, "addkey");
this.callingUser = callingUser;
this.user = user;
this.sshKey = sshKey;
this.gpgKeys = null;
}
@AssistedInject
public AddKeySender(EmailArguments ea,
IdentifiedUser callingUser,
@Assisted IdentifiedUser user,
@Assisted List<String> gpgKeys) {
super(ea, "addkey");
this.callingUser = callingUser;
this.user = user;
this.sshKey = null;
this.gpgKeys = gpgKeys;
}
@Override
protected void init() throws EmailException {
super.init();
setHeader("Subject",
String.format("[Gerrit Code Review] New %s Keys Added", getKeyType()));
add(RecipientType.TO, new Address(getEmail()));
}
@Override
protected boolean shouldSendMessage() {
/*
* Don't send an email if no keys are added, or an admin is adding a key to
* a user.
*/
return (sshKey != null || gpgKeys.size() > 0) &&
(user.equals(callingUser) ||
!callingUser.getCapabilities().canAdministrateServer());
}
@Override
protected void format() throws EmailException {
appendText(velocifyFile("AddKey.vm"));
}
public String getEmail() {
return user.getAccount().getPreferredEmail();
}
public String getUserNameEmail() {
return getUserNameEmailFor(user.getAccountId());
}
public String getKeyType() {
if (sshKey != null) {
return "SSH";
} else if (gpgKeys != null) {
return "GPG";
}
return "Unknown";
}
public String getSshKey() {
return (sshKey != null) ? sshKey.getSshPublicKey() + "\n" : null;
}
public String getGpgKeys() {
if (gpgKeys != null) {
return Joiner.on("\n").join(gpgKeys);
}
return null;
}
}

View File

@ -268,6 +268,13 @@ public abstract class OutgoingEmail {
return name;
}
/**
* Gets the human readable name and email for an account;
* if neither are available, returns the Anonymous Coward name.
*
* @param accountId user to fetch.
* @return name/email of account, or Anonymous Coward if unset.
*/
public String getNameEmailFor(Account.Id accountId) {
AccountState who = args.accountCache.get(accountId);
String name = who.getAccount().getFullName();
@ -286,6 +293,33 @@ public abstract class OutgoingEmail {
}
}
/**
* Gets the human readable name and email for an account;
* if both are unavailable, returns the username. If no
* username is set, this function returns null.
*
* @param accountId user to fetch.
* @return name/email of account, username, or null if unset.
*/
public String getUserNameEmailFor(Account.Id accountId) {
AccountState who = args.accountCache.get(accountId);
String name = who.getAccount().getFullName();
String email = who.getAccount().getPreferredEmail();
if (name != null && email != null) {
return name + " <" + email + ">";
} else if (email != null) {
return email;
} else if (name != null) {
return name;
}
String username = who.getUserName();
if (username != null) {
return username;
}
return null;
}
protected boolean shouldSendMessage() {
if (body.length() == 0) {
// If we have no message body, don't send.

View File

@ -58,22 +58,7 @@ public class RegisterNewEmailSender extends OutgoingEmail {
}
public String getUserNameEmail() {
String name = user.getAccount().getFullName();
String email = user.getAccount().getPreferredEmail();
if (name != null && email != null) {
return name + " <" + email + ">";
} else if (email != null) {
return email;
} else if (name != null) {
return name;
} else {
String username = user.getUserName();
if (username != null) {
return username;
}
}
return null;
return getUserNameEmailFor(user.getAccountId());
}
public String getEmailRegistrationToken() {

View File

@ -0,0 +1,61 @@
## 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.
##
##
## Template Type:
## -------------
## This is a velocity mail template, see: http://velocity.apache.org and the
## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates.
##
## Template File Names and extensions:
## ----------------------------------
## Gerrit will use templates ending in ".vm" but will ignore templates ending
## in ".vm.example". If a .vm template does not exist, the default internal
## gerrit template which is the same as the .vm.example will be used. If you
## want to override the default template, copy the .vm.example file to a .vm
## file and edit it appropriately.
##
## This Template:
## --------------
## The AddKey.vm template will determine the contents of the email
## related to adding a new SSH or GPG key to an account.
##
One or more new ${email.keyType} keys have been added to Gerrit Code Review at ${email.gerritHost}:
#if($email.sshKey)
$email.sshKey
#elseif($email.gpgKeys)
$email.gpgKeys
#end
If this is not expected, please contact your Gerrit Administrators
immediately.
You can also manage your ${email.keyType} keys by visiting
#if($email.sshKey)
$email.gerritUrl#/settings/ssh-keys
#elseif($email.gpgKeys)
$email.gerritUrl#/settings/gpg-keys
#end
#if($email.userNameEmail)
(while signed in as $email.userNameEmail)
#else
(while signed in as $email.email)
#end
If clicking the link above does not work, copy and paste the URL in a
new browser window instead.
This is a send-only email address. Replies to this message will not
be read or answered.