Fix --re=bob to match bob@example.com when using HTTP_LDAP

When we introduced HTTP_LDAP support we broke expansion of the local
username in situations like the --reviewer flag during receive.  This
was because the EmailExpander wasn't configured, as we usually rely on
the LDAP directory to return the address for us.

Bug: GERRIT-263
Change-Id: I8c8d5ce9e5ac1cd1a8b1dc85422257b00e1114ab
Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index aac7f87..13384e1 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -281,6 +281,12 @@
 low maxAge setting, to ensure LDAP modifications are picked up in
 a timely fashion.
 
+cache `"ldap_usernames"`::
++
+Caches a mapping of LDAP username to Gerrit account identity.  The
+cache automatically updates when a user first creates their account
+within Gerrit, so the cache expire time is largely irrelevant.
+
 cache `"openid"`::
 +
 If OpenID authentication is enabled, caches the OpenID discovery
diff --git a/src/main/java/com/google/gerrit/server/account/AccountManager.java b/src/main/java/com/google/gerrit/server/account/AccountManager.java
index 299f546..d45110e 100644
--- a/src/main/java/com/google/gerrit/server/account/AccountManager.java
+++ b/src/main/java/com/google/gerrit/server/account/AccountManager.java
@@ -221,6 +221,7 @@
     txn.commit();
 
     byEmailCache.evict(account.getPreferredEmail());
+    realm.onCreateAccount(who, account);
     return new AuthResult(newId, true);
   }
 
diff --git a/src/main/java/com/google/gerrit/server/account/AccountResolver.java b/src/main/java/com/google/gerrit/server/account/AccountResolver.java
index 3e229ee..ff36fae 100644
--- a/src/main/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/src/main/java/com/google/gerrit/server/account/AccountResolver.java
@@ -25,16 +25,15 @@
 import java.util.Set;
 
 public class AccountResolver {
-  private final EmailExpander emailExpander;
+  private final Realm realm;
   private final AccountByEmailCache byEmail;
   private final AccountCache byId;
   private final Provider<ReviewDb> schema;
 
   @Inject
-  AccountResolver(final EmailExpander emailExpander,
-      final AccountByEmailCache byEmail, final AccountCache byId,
-      final Provider<ReviewDb> schema) {
-    this.emailExpander = emailExpander;
+  AccountResolver(final Realm realm, final AccountByEmailCache byEmail,
+      final AccountCache byId, final Provider<ReviewDb> schema) {
+    this.realm = realm;
     this.byEmail = byEmail;
     this.byId = byId;
     this.schema = schema;
@@ -64,11 +63,9 @@
       return findByEmail(nameOrEmail);
     }
 
-    if (emailExpander.canExpand(nameOrEmail)) {
-      final Account a = findByEmail(emailExpander.expand(nameOrEmail));
-      if (a != null) {
-        return a;
-      }
+    final Account.Id id = realm.lookup(nameOrEmail);
+    if (id != null) {
+      return byId.get(id).getAccount();
     }
 
     return oneAccount(schema.get().accounts().byFullName(nameOrEmail));
diff --git a/src/main/java/com/google/gerrit/server/account/DefaultRealm.java b/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
index 9a833dd..5dbd445 100644
--- a/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
+++ b/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
@@ -22,10 +22,13 @@
 
 public final class DefaultRealm implements Realm {
   private final EmailExpander emailExpander;
+  private final AccountByEmailCache byEmail;
 
   @Inject
-  DefaultRealm(final EmailExpander emailExpander) {
+  DefaultRealm(final EmailExpander emailExpander,
+      final AccountByEmailCache byEmail) {
     this.emailExpander = emailExpander;
+    this.byEmail = byEmail;
   }
 
   @Override
@@ -43,7 +46,22 @@
   }
 
   @Override
+  public void onCreateAccount(final AuthRequest who, final Account account) {
+  }
+
+  @Override
   public Set<AccountGroup.Id> groups(final AccountState who) {
     return who.getInternalGroups();
   }
+
+  @Override
+  public Account.Id lookup(final String accountName) {
+    if (emailExpander.canExpand(accountName)) {
+      final Set<Account.Id> c = byEmail.get(emailExpander.expand(accountName));
+      if (1 == c.size()) {
+        return c.iterator().next();
+      }
+    }
+    return null;
+  }
 }
diff --git a/src/main/java/com/google/gerrit/server/account/Realm.java b/src/main/java/com/google/gerrit/server/account/Realm.java
index 37ba044..129a977 100644
--- a/src/main/java/com/google/gerrit/server/account/Realm.java
+++ b/src/main/java/com/google/gerrit/server/account/Realm.java
@@ -25,5 +25,17 @@
 
   public AuthRequest authenticate(AuthRequest who) throws AccountException;
 
+  public void onCreateAccount(AuthRequest who, Account account);
+
   public Set<AccountGroup.Id> groups(AccountState who);
+
+  /**
+   * Locate an account whose local username is the given account name.
+   * <p>
+   * Generally this only works for local realms, such as one backed by an LDAP
+   * directory, or where there is an {@link EmailExpander} configured that knows
+   * how to convert the accountName into an email address, and then locate the
+   * user by that email address.
+   */
+  public Account.Id lookup(String accountName);
 }
diff --git a/src/main/java/com/google/gerrit/server/ldap/LdapModule.java b/src/main/java/com/google/gerrit/server/ldap/LdapModule.java
index b708b77..9305756 100644
--- a/src/main/java/com/google/gerrit/server/ldap/LdapModule.java
+++ b/src/main/java/com/google/gerrit/server/ldap/LdapModule.java
@@ -16,6 +16,7 @@
 
 import static java.util.concurrent.TimeUnit.HOURS;
 
+import com.google.gerrit.client.reviewdb.Account;
 import com.google.gerrit.client.reviewdb.AccountGroup;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.cache.Cache;
@@ -26,14 +27,18 @@
 import java.util.Set;
 
 public class LdapModule extends CacheModule {
+  static final String USERNAME_CACHE = "ldap_usernames";
   static final String GROUP_CACHE = "ldap_groups";
 
   @Override
   protected void configure() {
-    final TypeLiteral<Cache<String, Set<AccountGroup.Id>>> type =
+    final TypeLiteral<Cache<String, Set<AccountGroup.Id>>> groups =
         new TypeLiteral<Cache<String, Set<AccountGroup.Id>>>() {};
+    final TypeLiteral<Cache<String, Account.Id>> usernames =
+        new TypeLiteral<Cache<String, Account.Id>>() {};
 
-    core(type, GROUP_CACHE).timeToIdle(1, HOURS).timeToLive(1, HOURS);
+    core(groups, GROUP_CACHE).timeToIdle(1, HOURS).timeToLive(1, HOURS);
+    core(usernames, USERNAME_CACHE);
     bind(Realm.class).to(LdapRealm.class).in(Scopes.SINGLETON);
   }
 }
diff --git a/src/main/java/com/google/gerrit/server/ldap/LdapRealm.java b/src/main/java/com/google/gerrit/server/ldap/LdapRealm.java
index 90f7297..2d0d2a0 100644
--- a/src/main/java/com/google/gerrit/server/ldap/LdapRealm.java
+++ b/src/main/java/com/google/gerrit/server/ldap/LdapRealm.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.client.reviewdb.Account;
 import com.google.gerrit.client.reviewdb.AccountExternalId;
 import com.google.gerrit.client.reviewdb.AccountGroup;
+import com.google.gerrit.client.reviewdb.ReviewDb;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.AuthRequest;
@@ -26,6 +27,8 @@
 import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.SelfPopulatingCache;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import com.google.inject.name.Named;
@@ -57,11 +60,13 @@
   private final String username;
   private final String password;
 
+  private final SchemaFactory<ReviewDb> schema;
   private final EmailExpander emailExpander;
   private final String accountFullName;
   private final String accountEmailAddress;
   private final String accountSshUserName;
   private final LdapQuery accountQuery;
+  private final SelfPopulatingCache<String, Account.Id> usernameCache;
 
   private final GroupCache groupCache;
   private final String groupName;
@@ -73,10 +78,13 @@
   LdapRealm(
       final GroupCache groupCache,
       final EmailExpander emailExpander,
+      final SchemaFactory<ReviewDb> schema,
       @Named(LdapModule.GROUP_CACHE) final Cache<String, Set<AccountGroup.Id>> rawGroup,
+      @Named(LdapModule.USERNAME_CACHE) final Cache<String, Account.Id> rawUsername,
       @GerritServerConfig final Config config) {
-    this.emailExpander = emailExpander;
     this.groupCache = groupCache;
+    this.emailExpander = emailExpander;
+    this.schema = schema;
 
     this.server = required(config, "server");
     this.username = optional(config, "username");
@@ -138,6 +146,13 @@
     if (accountQuery.getParameters().length == 0) {
       throw new IllegalArgumentException("No variables in ldap.accountPattern");
     }
+
+    usernameCache = new SelfPopulatingCache<String, Account.Id>(rawUsername) {
+      @Override
+      public Account.Id createEntry(final String username) throws Exception {
+        return queryForUsername(username);
+      }
+    };
   }
 
   private static String optional(final Config config, final String name) {
@@ -230,6 +245,11 @@
   }
 
   @Override
+  public void onCreateAccount(final AuthRequest who, final Account account) {
+    usernameCache.put(who.getLocalUser(), account.getId());
+  }
+
+  @Override
   public Set<AccountGroup.Id> groups(final AccountState who) {
     final HashSet<AccountGroup.Id> r = new HashSet<AccountGroup.Id>();
     r.addAll(membershipCache.get(findId(who.getExternalIds())));
@@ -293,6 +313,31 @@
     return null;
   }
 
+  @Override
+  public Account.Id lookup(final String accountName) {
+    return usernameCache.get(accountName);
+  }
+
+  private Account.Id queryForUsername(final String username) {
+    try {
+      final ReviewDb db = schema.open();
+      try {
+        final List<AccountExternalId> candidates =
+            db.accountExternalIds().byExternal(
+                AccountExternalId.SCHEME_GERRIT + username).toList();
+        if (candidates.size() == 1) {
+          return candidates.get(0).getAccountId();
+        }
+        return null;
+      } finally {
+        db.close();
+      }
+    } catch (OrmException e) {
+      log.warn("Cannot query for username in database", e);
+      return null;
+    }
+  }
+
   private DirContext open() throws NamingException {
     final Properties env = new Properties();
     env.put(Context.INITIAL_CONTEXT_FACTORY, LDAP);