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
diff --git a/Documentation/config-mail.txt b/Documentation/config-mail.txt
index 62d0219..da213a8 100644
--- a/Documentation/config-mail.txt
+++ b/Documentation/config-mail.txt
@@ -28,6 +28,12 @@
 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
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
index 670adba..80e3500 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
@@ -27,6 +27,7 @@
 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.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.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 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 @@
       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 @@
               "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 @@
         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:
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 8a227ac..6270a15 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
@@ -100,6 +100,7 @@
     chmod(0700, site.tmp_dir);
 
     extractMailExample("Abandoned.vm");
+    extractMailExample("AddKey.vm");
     extractMailExample("ChangeFooter.vm");
     extractMailExample("ChangeSubject.vm");
     extractMailExample("Comment.vm");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
index 3c21d17..7ec659e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
@@ -18,6 +18,7 @@
 
 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.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.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 @@
   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 @@
           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) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index cf1053d..3964115 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -95,6 +95,7 @@
 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 @@
 
     factory(AccountInfoCacheFactory.Factory.class);
     factory(AddReviewerSender.Factory.class);
+    factory(AddKeySender.Factory.class);
     factory(CapabilityControl.Factory.class);
     factory(ChangeData.Factory.class);
     factory(ChangeJson.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddKeySender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddKeySender.java
new file mode 100644
index 0000000..0f1e86e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddKeySender.java
@@ -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;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
index 1e4fec7..a2f369b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
@@ -268,6 +268,13 @@
     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 @@
     }
   }
 
+  /**
+   * 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.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
index eb32700..beada69 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
@@ -58,22 +58,7 @@
   }
 
   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() {
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKey.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKey.vm
new file mode 100644
index 0000000..c60ce8b
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKey.vm
@@ -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.