Merge "Expose extension point for generic OAuth providers" into stable-2.10
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index f121e1f..4ad7d21 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -2282,6 +2282,16 @@
 Default is unset for RFC 2307 servers (disabled)
 and `memberOf` for Active Directory.
 
+[[ldap.fetchMemberOfEagerly]]ldap.fetchMemberOfEagerly::
++
+_(Optional)_ Whether to fetch the `memberOf` account attribute on
+login. Setups which use LDAP for user authentication but don't make
+use of the LDAP groups may benefit from setting this option to `false`
+as this will result in a much faster LDAP login.
++
+Default is unset for RFC 2307 servers (disabled) and `true` for
+Active Directory.
+
 [[ldap.groupBase]]ldap.groupBase::
 +
 Root of the tree containing all group objects.  This is typically
@@ -2392,6 +2402,47 @@
 must have the DWORD value `allowtgtsessionkey` set to 1 and the account must not
 have local administrator privileges.
 
+[[ldap.useConnectionPooling]]ldap.useConnectionPooling::
++
+_(Optional)_ Enable the LDAP connection pooling or not.
++
+If it is true, the LDAP service provider maintains a pool of (possibly)
+previously used connections and assigns them to a Context instance as
+needed. When a Context instance is done with a connection (closed or
+garbage collected), the connection is returned to the pool for future use.
++
+For details, see link:http://docs.oracle.com/javase/tutorial/jndi/ldap/pool.html[
+LDAP connection management (Pool)] and link:http://docs.oracle.com/javase/tutorial/jndi/ldap/config.html[
+LDAP connection management (Configuration)]
++
+By default, false.
+
+[[ldap.connectTimeout]]ldap.connectTimeout::
++
+_(Optional)_ Timeout period for establishment of an LDAP connection.
++
+The value is in the usual time-unit format like "1 s", "100 ms",
+etc...
++
+By default there is no timeout and Gerrit will wait indefinitely.
+
+[[ldap-connection-pooling]]
+==== LDAP Connection Pooling
+Once LDAP connection pooling is enabled by setting the link:#ldap.useConnectionPooling[
+ldap.useConnectionPooling] configuration property to `true`, the connection pool
+can be configured using JVM system properties as explained in the
+link:http://docs.oracle.com/javase/7/docs/technotes/guides/jndi/jndi-ldap.html#POOL[
+Java SE Documentation].
+
+For standalone Gerrit (running with the embedded Jetty), JVM system properties
+are specified in the link:#container[container section]:
+
+----
+  javaOptions = -Dcom.sun.jndi.ldap.connect.pool.maxsize=20
+  javaOptions = -Dcom.sun.jndi.ldap.connect.pool.prefsize=10
+  javaOptions = -Dcom.sun.jndi.ldap.connect.pool.timeout=300000
+----
+
 [[mimetype]]
 === Section mimetype
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCache.java
index d130243..6ba6bf0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCache.java
@@ -21,14 +21,14 @@
 /** Tracks group inclusions in memory for efficient access. */
 public interface GroupIncludeCache {
   /** @return groups directly a member of the passed group. */
-  public Set<AccountGroup.UUID> membersOf(AccountGroup.UUID group);
+  public Set<AccountGroup.UUID> subgroupsOf(AccountGroup.UUID group);
 
   /** @return any groups the passed group belongs to. */
-  public Set<AccountGroup.UUID> memberIn(AccountGroup.UUID groupId);
+  public Set<AccountGroup.UUID> parentGroupsOf(AccountGroup.UUID groupId);
 
   /** @return set of any UUIDs that are not internal groups. */
   public Set<AccountGroup.UUID> allExternalMembers();
 
-  public void evictMembersOf(AccountGroup.UUID groupId);
-  public void evictMemberIn(AccountGroup.UUID groupId);
+  public void evictSubgroupsOf(AccountGroup.UUID groupId);
+  public void evictParentGroupsOf(AccountGroup.UUID groupId);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
index 37d407c..9e7918d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
@@ -42,23 +42,23 @@
 public class GroupIncludeCacheImpl implements GroupIncludeCache {
   private static final Logger log = LoggerFactory
       .getLogger(GroupIncludeCacheImpl.class);
-  private static final String BYINCLUDE_NAME = "groups_byinclude";
-  private static final String MEMBERS_NAME = "groups_members";
+  private static final String PARENT_GROUPS_NAME = "groups_byinclude";
+  private static final String SUBGROUPS_NAME = "groups_members";
   private static final String EXTERNAL_NAME = "groups_external";
 
   public static Module module() {
     return new CacheModule() {
       @Override
       protected void configure() {
-        cache(BYINCLUDE_NAME,
+        cache(PARENT_GROUPS_NAME,
             AccountGroup.UUID.class,
             new TypeLiteral<Set<AccountGroup.UUID>>() {})
-          .loader(MemberInLoader.class);
+          .loader(ParentGroupsLoader.class);
 
-        cache(MEMBERS_NAME,
+        cache(SUBGROUPS_NAME,
             AccountGroup.UUID.class,
             new TypeLiteral<Set<AccountGroup.UUID>>() {})
-          .loader(MembersOfLoader.class);
+          .loader(SubgroupsLoader.class);
 
         cache(EXTERNAL_NAME,
             String.class,
@@ -71,24 +71,24 @@
     };
   }
 
-  private final LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> membersOf;
-  private final LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> memberIn;
+  private final LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> subgroups;
+  private final LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> parentGroups;
   private final LoadingCache<String, Set<AccountGroup.UUID>> external;
 
   @Inject
   GroupIncludeCacheImpl(
-      @Named(MEMBERS_NAME) LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> membersOf,
-      @Named(BYINCLUDE_NAME) LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> memberIn,
+      @Named(SUBGROUPS_NAME) LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> subgroups,
+      @Named(PARENT_GROUPS_NAME) LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> parentGroups,
       @Named(EXTERNAL_NAME) LoadingCache<String, Set<AccountGroup.UUID>> external) {
-    this.membersOf = membersOf;
-    this.memberIn = memberIn;
+    this.subgroups = subgroups;
+    this.parentGroups = parentGroups;
     this.external = external;
   }
 
   @Override
-  public Set<AccountGroup.UUID> membersOf(AccountGroup.UUID groupId) {
+  public Set<AccountGroup.UUID> subgroupsOf(AccountGroup.UUID groupId) {
     try {
-      return membersOf.get(groupId);
+      return subgroups.get(groupId);
     } catch (ExecutionException e) {
       log.warn("Cannot load members of group", e);
       return Collections.emptySet();
@@ -96,9 +96,9 @@
   }
 
   @Override
-  public Set<AccountGroup.UUID> memberIn(AccountGroup.UUID groupId) {
+  public Set<AccountGroup.UUID> parentGroupsOf(AccountGroup.UUID groupId) {
     try {
-      return memberIn.get(groupId);
+      return parentGroups.get(groupId);
     } catch (ExecutionException e) {
       log.warn("Cannot load included groups", e);
       return Collections.emptySet();
@@ -106,16 +106,16 @@
   }
 
   @Override
-  public void evictMembersOf(AccountGroup.UUID groupId) {
+  public void evictSubgroupsOf(AccountGroup.UUID groupId) {
     if (groupId != null) {
-      membersOf.invalidate(groupId);
+      subgroups.invalidate(groupId);
     }
   }
 
   @Override
-  public void evictMemberIn(AccountGroup.UUID groupId) {
+  public void evictParentGroupsOf(AccountGroup.UUID groupId) {
     if (groupId != null) {
-      memberIn.invalidate(groupId);
+      parentGroups.invalidate(groupId);
 
       if (!AccountGroup.isInternalGroup(groupId)) {
         external.invalidate(EXTERNAL_NAME);
@@ -133,12 +133,12 @@
     }
   }
 
-  static class MembersOfLoader extends
+  static class SubgroupsLoader extends
       CacheLoader<AccountGroup.UUID, Set<AccountGroup.UUID>> {
     private final SchemaFactory<ReviewDb> schema;
 
     @Inject
-    MembersOfLoader(final SchemaFactory<ReviewDb> sf) {
+    SubgroupsLoader(final SchemaFactory<ReviewDb> sf) {
       schema = sf;
     }
 
@@ -163,12 +163,12 @@
     }
   }
 
-  static class MemberInLoader extends
+  static class ParentGroupsLoader extends
       CacheLoader<AccountGroup.UUID, Set<AccountGroup.UUID>> {
     private final SchemaFactory<ReviewDb> schema;
 
     @Inject
-    MemberInLoader(final SchemaFactory<ReviewDb> sf) {
+    ParentGroupsLoader(final SchemaFactory<ReviewDb> sf) {
       schema = sf;
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
index 2e278c3..b8a67ff 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
@@ -90,7 +90,7 @@
         }
 
         memberOf.put(id, false);
-        if (search(includeCache.membersOf(id))) {
+        if (search(includeCache.subgroupsOf(id))) {
           memberOf.put(id, true);
           return true;
         }
@@ -131,7 +131,7 @@
 
     while (!q.isEmpty()) {
       AccountGroup.UUID id = q.remove(q.size() - 1);
-      for (AccountGroup.UUID g : includeCache.memberIn(id)) {
+      for (AccountGroup.UUID g : includeCache.parentGroupsOf(id)) {
         if (g != null && r.add(g)) {
           q.add(g);
           memberOf.put(g, true);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
index a54a97b..86c840b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
@@ -116,7 +116,7 @@
 
     if (createGroupArgs.initialGroups != null) {
       addGroups(groupId, createGroupArgs.initialGroups);
-      groupIncludeCache.evictMembersOf(uuid);
+      groupIncludeCache.evictSubgroupsOf(uuid);
     }
 
     groupCache.onCreateGroup(createGroupArgs.getGroup());
@@ -162,7 +162,7 @@
     db.accountGroupByIdAud().insert(includesAudit);
 
     for (AccountGroup.UUID uuid : groups) {
-      groupIncludeCache.evictMemberIn(uuid);
+      groupIncludeCache.evictParentGroupsOf(uuid);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
index 5a19814..f8ecf25 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Throwables;
 import com.google.common.cache.Cache;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.ParameterizedString;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.AccountException;
@@ -37,6 +38,7 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Properties;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
@@ -58,7 +60,7 @@
 @Singleton class Helper {
   static final String LDAP_UUID = "ldap:";
 
-  private final Cache<String, ImmutableSet<String>> groupsByInclude;
+  private final Cache<String, ImmutableSet<String>> parentGroups;
   private final Config config;
   private final String server;
   private final String username;
@@ -67,28 +69,41 @@
   private final boolean sslVerify;
   private final String authentication;
   private volatile LdapSchema ldapSchema;
-  private final String readTimeOutMillis;
+  private final String readTimeoutMillis;
+  private final String connectTimeoutMillis;
+  private final boolean useConnectionPooling;
 
   @Inject
   Helper(@GerritServerConfig final Config config,
-      @Named(LdapModule.GROUPS_BYINCLUDE_CACHE)
-      Cache<String, ImmutableSet<String>> groupsByInclude) {
+      @Named(LdapModule.PARENT_GROUPS_CACHE)
+      Cache<String, ImmutableSet<String>> parentGroups) {
     this.config = config;
     this.server = LdapRealm.optional(config, "server");
     this.username = LdapRealm.optional(config, "username");
-    this.password = LdapRealm.optional(config, "password");
-    this.referral = LdapRealm.optional(config, "referral");
+    this.password = LdapRealm.optional(config, "password", "");
+    this.referral = LdapRealm.optional(config, "referral", "ignore");
     this.sslVerify = config.getBoolean("ldap", "sslverify", true);
-    this.authentication = LdapRealm.optional(config, "authentication");
-    String timeout = LdapRealm.optional(config, "readTimeout");
-    if (timeout != null) {
-      readTimeOutMillis =
-          Long.toString(ConfigUtil.getTimeUnit(timeout, 0,
+    this.authentication =
+        LdapRealm.optional(config, "authentication", "simple");
+    String readTimeout = LdapRealm.optional(config, "readTimeout");
+    if (readTimeout != null) {
+      readTimeoutMillis =
+          Long.toString(ConfigUtil.getTimeUnit(readTimeout, 0,
               TimeUnit.MILLISECONDS));
     } else {
-      readTimeOutMillis = null;
+      readTimeoutMillis = null;
     }
-    this.groupsByInclude = groupsByInclude;
+    String connectTimeout = LdapRealm.optional(config, "connectTimeout");
+    if (connectTimeout != null) {
+      connectTimeoutMillis =
+          Long.toString(ConfigUtil.getTimeUnit(connectTimeout, 0,
+              TimeUnit.MILLISECONDS));
+    } else {
+      connectTimeoutMillis = null;
+    }
+    this.parentGroups = parentGroups;
+    this.useConnectionPooling =
+        LdapRealm.optional(config, "useConnectionPooling", false);
   }
 
   private Properties createContextProperties() {
@@ -99,22 +114,28 @@
       Class<? extends SSLSocketFactory> factory = BlindSSLSocketFactory.class;
       env.put("java.naming.ldap.factory.socket", factory.getName());
     }
-    if (readTimeOutMillis != null) {
-      env.put("com.sun.jndi.ldap.read.timeout", readTimeOutMillis);
+    if (readTimeoutMillis != null) {
+      env.put("com.sun.jndi.ldap.read.timeout", readTimeoutMillis);
+    }
+    if (connectTimeoutMillis != null) {
+      env.put("com.sun.jndi.ldap.connect.timeout", connectTimeoutMillis);
+    }
+    if (useConnectionPooling) {
+      env.put("com.sun.jndi.ldap.connect.pool", "true");
     }
     return env;
   }
 
   DirContext open() throws NamingException, LoginException {
     final Properties env = createContextProperties();
-    env.put(Context.SECURITY_AUTHENTICATION, authentication != null ? authentication : "simple");
-    env.put(Context.REFERRAL, referral != null ? referral : "ignore");
+    env.put(Context.SECURITY_AUTHENTICATION, authentication);
+    env.put(Context.REFERRAL, referral);
     if ("GSSAPI".equals(authentication)) {
       return kerberosOpen(env);
     } else {
       if (username != null) {
         env.put(Context.SECURITY_PRINCIPAL, username);
-        env.put(Context.SECURITY_CREDENTIALS, password != null ? password : "");
+        env.put(Context.SECURITY_CREDENTIALS, password);
       }
       return new InitialDirContext(env);
     }
@@ -146,8 +167,8 @@
     final Properties env = createContextProperties();
     env.put(Context.SECURITY_AUTHENTICATION, "simple");
     env.put(Context.SECURITY_PRINCIPAL, dn);
-    env.put(Context.SECURITY_CREDENTIALS, password != null ? password : "");
-    env.put(Context.REFERRAL, referral != null ? referral : "ignore");
+    env.put(Context.SECURITY_CREDENTIALS, password);
+    env.put(Context.REFERRAL, referral);
     try {
       return new InitialDirContext(env);
     } catch (NamingException e) {
@@ -166,27 +187,28 @@
     return ldapSchema;
   }
 
-  LdapQuery.Result findAccount(final Helper.LdapSchema schema,
-      final DirContext ctx, final String username) throws NamingException,
-      AccountException {
+  LdapQuery.Result findAccount(Helper.LdapSchema schema,
+      DirContext ctx, String username, boolean fetchMemberOf)
+      throws NamingException, AccountException {
     final HashMap<String, String> params = new HashMap<>();
     params.put(LdapRealm.USERNAME, username);
 
-    final List<LdapQuery.Result> res = new ArrayList<>();
-    for (LdapQuery accountQuery : schema.accountQueryList) {
-      res.addAll(accountQuery.query(ctx, params));
+    List<LdapQuery> accountQueryList;
+    if (fetchMemberOf && schema.type.accountMemberField() != null) {
+      accountQueryList = schema.accountWithMemberOfQueryList;
+    } else {
+      accountQueryList = schema.accountQueryList;
     }
 
-    switch (res.size()) {
-      case 0:
-        throw new NoSuchUserException(username);
-
-      case 1:
+    for (LdapQuery accountQuery : accountQueryList) {
+      List<LdapQuery.Result> res = accountQuery.query(ctx, params);
+      if (res.size() == 1) {
         return res.get(0);
-
-      default:
+      } else if (res.size() > 1) {
         throw new AccountException("Duplicate users: " + username);
+      }
     }
+    throw new NoSuchUserException(username);
   }
 
   Set<AccountGroup.UUID> queryForGroups(final DirContext ctx,
@@ -200,7 +222,7 @@
 
       if (account == null) {
         try {
-          account = findAccount(schema, ctx, username);
+          account = findAccount(schema, ctx, username, false);
         } catch (AccountException e) {
           LdapRealm.log.warn("Account " + username +
               " not found, assuming empty group membership");
@@ -221,9 +243,9 @@
     }
 
     if (schema.accountMemberField != null) {
-      if (account == null) {
+      if (account == null || account.getAll(schema.accountMemberField) == null) {
         try {
-          account = findAccount(schema, ctx, username);
+          account = findAccount(schema, ctx, username, true);
         } catch (AccountException e) {
           LdapRealm.log.warn("Account " + username +
               " not found, assuming empty group membership");
@@ -259,14 +281,15 @@
   private void recursivelyExpandGroups(final Set<String> groupDNs,
       final LdapSchema schema, final DirContext ctx, final String groupDN) {
     if (groupDNs.add(groupDN) && schema.accountMemberField != null) {
-      ImmutableSet<String> cachedGroupDNs = groupsByInclude.getIfPresent(groupDN);
-      if (cachedGroupDNs == null) {
+      ImmutableSet<String> cachedParentsDNs = parentGroups.getIfPresent(groupDN);
+      if (cachedParentsDNs == null) {
         // Recursively identify the groups it is a member of.
         ImmutableSet.Builder<String> dns = ImmutableSet.builder();
         try {
           final Name compositeGroupName = new CompositeName().add(groupDN);
           final Attribute in =
-              ctx.getAttributes(compositeGroupName).get(schema.accountMemberField);
+              ctx.getAttributes(compositeGroupName, schema.accountMemberFieldArray)
+                .get(schema.accountMemberField);
           if (in != null) {
             final NamingEnumeration<?> groups = in.getAll();
             try {
@@ -279,10 +302,10 @@
         } catch (NamingException e) {
           LdapRealm.log.warn("Could not find group " + groupDN, e);
         }
-        cachedGroupDNs = dns.build();
-        groupsByInclude.put(groupDN, cachedGroupDNs);
+        cachedParentsDNs = dns.build();
+        parentGroups.put(groupDN, cachedParentsDNs);
       }
-      for (String dn : cachedGroupDNs) {
+      for (String dn : cachedParentsDNs) {
         recursivelyExpandGroups(groupDNs, schema, ctx, dn);
       }
     }
@@ -295,7 +318,9 @@
     final ParameterizedString accountEmailAddress;
     final ParameterizedString accountSshUserName;
     final String accountMemberField;
+    final String[] accountMemberFieldArray;
     final List<LdapQuery> accountQueryList;
+    final List<LdapQuery> accountWithMemberOfQueryList;
 
     final List<String> groupBases;
     final SearchScope groupScope;
@@ -307,6 +332,7 @@
       type = discoverLdapType(ctx);
       groupMemberQueryList = new ArrayList<>();
       accountQueryList = new ArrayList<>();
+      accountWithMemberOfQueryList = new ArrayList<>();
 
       final Set<String> accountAtts = new HashSet<>();
 
@@ -359,15 +385,24 @@
       accountMemberField =
           LdapRealm.optdef(config, "accountMemberField", type.accountMemberField());
       if (accountMemberField != null) {
-        accountAtts.add(accountMemberField);
+        accountMemberFieldArray = new String[] {accountMemberField};
+      } else {
+        accountMemberFieldArray = null;
       }
 
       final SearchScope accountScope = LdapRealm.scope(config, "accountScope");
       final String accountPattern =
           LdapRealm.reqdef(config, "accountPattern", type.accountPattern());
 
+      Set<String> accountWithMemberOfAtts;
+      if (accountMemberField != null) {
+        accountWithMemberOfAtts = new HashSet<>(accountAtts);
+        accountWithMemberOfAtts.add(accountMemberField);
+      } else {
+        accountWithMemberOfAtts = null;
+      }
       for (String accountBase : LdapRealm.requiredList(config, "accountBase")) {
-        final LdapQuery accountQuery =
+        LdapQuery accountQuery =
             new LdapQuery(accountBase, accountScope, new ParameterizedString(
                 accountPattern), accountAtts);
         if (accountQuery.getParameters().isEmpty()) {
@@ -375,6 +410,13 @@
               "No variables in ldap.accountPattern");
         }
         accountQueryList.add(accountQuery);
+
+        if (accountWithMemberOfAtts != null) {
+          LdapQuery accountWithMemberOfQuery =
+              new LdapQuery(accountBase, accountScope, new ParameterizedString(
+                  accountPattern), accountWithMemberOfAtts);
+          accountWithMemberOfQueryList.add(accountWithMemberOfQuery);
+        }
       }
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java
index eb6249c..1d90f0c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java
@@ -83,7 +83,7 @@
       }
       try {
         final Helper.LdapSchema schema = helper.getSchema(ctx);
-        final LdapQuery.Result m = helper.findAccount(schema, ctx, username);
+        final LdapQuery.Result m = helper.findAccount(schema, ctx, username, false);
 
         if (authConfig.getAuthType() == AuthType.LDAP) {
           // We found the user account, but we need to verify
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
index 7731b7d..6cdce8d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
@@ -31,7 +31,6 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupMembership;
-import com.google.gerrit.server.account.ListGroupMembership;
 import com.google.gerrit.server.auth.ldap.Helper.LdapSchema;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
@@ -60,7 +59,7 @@
  * Implementation of GroupBackend for the LDAP group system.
  */
 public class LdapGroupBackend implements GroupBackend {
-  private static final Logger log = LoggerFactory.getLogger(LdapGroupBackend.class);
+  static final Logger log = LoggerFactory.getLogger(LdapGroupBackend.class);
 
   private static final String LDAP_NAME = "ldap/";
   private static final String GROUPNAME = "groupname";
@@ -185,21 +184,7 @@
     if (id == null) {
       return GroupMembership.EMPTY;
     }
-
-    try {
-      final Set<AccountGroup.UUID> groups = membershipCache.get(id);
-      return new ListGroupMembership(groups) {
-        @Override
-        public Set<AccountGroup.UUID> getKnownGroups() {
-          Set<AccountGroup.UUID> g = Sets.newHashSet(groups);
-          g.retainAll(projectCache.guessRelevantGroupUUIDs());
-          return g;
-        }
-      };
-    } catch (ExecutionException e) {
-      log.warn(String.format("Cannot lookup membershipsOf %s in LDAP", id), e);
-      return GroupMembership.EMPTY;
-    }
+    return new LdapGroupMembership(membershipCache, projectCache, id);
   }
 
   private static String findId(final Collection<AccountExternalId> ids) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupMembership.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupMembership.java
new file mode 100644
index 0000000..7853320
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupMembership.java
@@ -0,0 +1,76 @@
+// 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.auth.ldap;
+
+import com.google.common.cache.LoadingCache;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.GroupMembership;
+import com.google.gerrit.server.account.ListGroupMembership;
+import com.google.gerrit.server.project.ProjectCache;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+
+class LdapGroupMembership implements GroupMembership {
+  private final LoadingCache<String, Set<AccountGroup.UUID>> membershipCache;
+  private final ProjectCache projectCache;
+  private final String id;
+  private GroupMembership membership;
+
+  LdapGroupMembership(
+      LoadingCache<String, Set<AccountGroup.UUID>> membershipCache,
+      ProjectCache projectCache,
+      String id) {
+    this.membershipCache = membershipCache;
+    this.projectCache = projectCache;
+    this.id = id;
+  }
+
+  @Override
+  public boolean contains(AccountGroup.UUID groupId) {
+    return get().contains(groupId);
+  }
+
+  @Override
+  public boolean containsAnyOf(Iterable<AccountGroup.UUID> groupIds) {
+    return get().containsAnyOf(groupIds);
+  }
+
+  @Override
+  public Set<AccountGroup.UUID> intersection(Iterable<AccountGroup.UUID> groupIds) {
+    return get().intersection(groupIds);
+  }
+
+  @Override
+  public Set<AccountGroup.UUID> getKnownGroups() {
+    Set<AccountGroup.UUID> g = new HashSet<>(get().getKnownGroups());
+    g.retainAll(projectCache.guessRelevantGroupUUIDs());
+    return g;
+  }
+
+  private synchronized GroupMembership get() {
+    if (membership == null) {
+      try {
+        membership = new ListGroupMembership(membershipCache.get(id));
+      } catch (ExecutionException e) {
+        LdapGroupBackend.log.warn(String.format(
+            "Cannot lookup membershipsOf %s in LDAP", id), e);
+        membership = GroupMembership.EMPTY;
+      }
+    }
+    return membership;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
index 88cf45b..eaaafd6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
@@ -33,7 +33,7 @@
   static final String USERNAME_CACHE = "ldap_usernames";
   static final String GROUP_CACHE = "ldap_groups";
   static final String GROUP_EXIST_CACHE = "ldap_group_existence";
-  static final String GROUPS_BYINCLUDE_CACHE = "ldap_groups_byinclude";
+  static final String PARENT_GROUPS_CACHE = "ldap_groups_byinclude";
 
 
   @Override
@@ -55,7 +55,7 @@
       .expireAfterWrite(1, HOURS)
       .loader(LdapRealm.ExistenceLoader.class);
 
-    cache(GROUPS_BYINCLUDE_CACHE,
+    cache(PARENT_GROUPS_CACHE,
         String.class,
         new TypeLiteral<ImmutableSet<String>>() {})
       .expireAfterWrite(1, HOURS);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index 8bb481c..7b79add 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -69,6 +69,7 @@
   private final EmailExpander emailExpander;
   private final LoadingCache<String, Optional<Account.Id>> usernameCache;
   private final Set<Account.FieldName> readOnlyAccountFields;
+  private final boolean fetchMemberOfEagerly;
   private final Config config;
 
   private final LoadingCache<String, Set<AccountGroup.UUID>> membershipCache;
@@ -96,6 +97,8 @@
     if (optdef(config, "accountSshUserName", "DEFAULT") != null) {
       readOnlyAccountFields.add(Account.FieldName.USER_NAME);
     }
+
+    fetchMemberOfEagerly = optional(config, "fetchMemberOfEagerly", true);
   }
 
   static SearchScope scope(final Config c, final String setting) {
@@ -106,6 +109,22 @@
     return config.getString("ldap", null, name);
   }
 
+  static int optional(Config config, String name, int defaultValue) {
+    return config.getInt("ldap", name, defaultValue);
+  }
+
+  static String optional(Config config, String name, String defaultValue) {
+    final String v = optional(config, name);
+    if (Strings.isNullOrEmpty(v)) {
+      return defaultValue;
+    }
+    return v;
+  }
+
+  static boolean optional(Config config, String name, boolean defaultValue) {
+    return config.getBoolean("ldap", name, defaultValue);
+  }
+
   static String required(final Config config, final String name) {
     final String v = optional(config, name);
     if (v == null || "".equals(v)) {
@@ -200,7 +219,8 @@
       }
       try {
         final Helper.LdapSchema schema = helper.getSchema(ctx);
-        final LdapQuery.Result m = helper.findAccount(schema, ctx, username);
+        final LdapQuery.Result m = helper.findAccount(schema, ctx, username,
+            fetchMemberOfEagerly);
 
         if (authConfig.getAuthType() == AuthType.LDAP && !who.isSkipAuthentication()) {
           // We found the user account, but we need to verify
@@ -229,7 +249,9 @@
         // in the middle of authenticating the user, its likely we will
         // need to know what access rights they have soon.
         //
-        membershipCache.put(username, helper.queryForGroups(ctx, username, m));
+        if (fetchMemberOfEagerly) {
+          membershipCache.put(username, helper.queryForGroups(ctx, username, m));
+        }
         return who;
       } finally {
         try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
index 614138a..163b335 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
@@ -128,9 +128,9 @@
       db.get().accountGroupByIdAud().insert(newIncludedGroupsAudits);
       db.get().accountGroupById().insert(newIncludedGroups.values());
       for (AccountGroupById agi : newIncludedGroups.values()) {
-        groupIncludeCache.evictMemberIn(agi.getIncludeUUID());
+        groupIncludeCache.evictParentGroupsOf(agi.getIncludeUUID());
       }
-      groupIncludeCache.evictMembersOf(group.getGroupUUID());
+      groupIncludeCache.evictSubgroupsOf(group.getGroupUUID());
     }
 
     return result;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
index 555744e..8cc1a4a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
@@ -91,9 +91,9 @@
       writeAudits(toRemove);
       db.get().accountGroupById().delete(toRemove);
       for (final AccountGroupById g : toRemove) {
-        groupIncludeCache.evictMemberIn(g.getIncludeUUID());
+        groupIncludeCache.evictParentGroupsOf(g.getIncludeUUID());
       }
-      groupIncludeCache.evictMembersOf(internalGroup.getGroupUUID());
+      groupIncludeCache.evictSubgroupsOf(internalGroup.getGroupUUID());
     }
 
     return Response.none();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
index 41dfba5..f067e27 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
@@ -436,7 +436,10 @@
         @Override
         public Integer get(ChangeData input, FillArgs args)
             throws OrmException {
-          return input.changedLines().insertions;
+
+          return input.changedLines() != null
+              ? input.changedLines().insertions
+              : null;
         }
       };
 
@@ -447,7 +450,9 @@
         @Override
         public Integer get(ChangeData input, FillArgs args)
             throws OrmException {
-          return input.changedLines().deletions;
+          return input.changedLines() != null
+              ? input.changedLines().deletions
+              : null;
         }
       };
 
@@ -459,7 +464,9 @@
         public Integer get(ChangeData input, FillArgs args)
             throws OrmException {
           ChangedLines changedLines = input.changedLines();
-          return changedLines.insertions + changedLines.deletions;
+          return changedLines != null
+              ? changedLines.insertions + changedLines.deletions
+              : null;
         }
       };
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
index 3d16372..872b84a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
@@ -166,7 +166,7 @@
       for (AccountGroupMember m : db.accountGroupMembers().byGroup(ig.getId())) {
         matching.accounts.add(m.getAccountId());
       }
-      for (AccountGroup.UUID m : args.groupIncludes.membersOf(uuid)) {
+      for (AccountGroup.UUID m : args.groupIncludes.subgroupsOf(uuid)) {
         if (seen.add(m)) {
           q.add(m);
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
index 7c8b29f..b09e39f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
@@ -32,10 +32,10 @@
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.Repository;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
@@ -72,30 +72,34 @@
     }
 
     try {
-      final Map<String, Ref> all = db.getRefDatabase().getRefs(RefDatabase.ALL);
+      List<Ref> refs =
+          new ArrayList<>(db.getRefDatabase().getRefs(Constants.R_HEADS)
+              .values());
 
-      if (!all.containsKey(Constants.HEAD)) {
-        // The branch pointed to by HEAD doesn't exist yet, so getAllRefs
-        // filtered it out. If we ask for it individually we can find the
-        // underlying target and put it into the map anyway.
-        //
         try {
           Ref head = db.getRef(Constants.HEAD);
           if (head != null) {
-            all.put(Constants.HEAD, head);
+            refs.add(head);
           }
         } catch (IOException e) {
           // Ignore the failure reading HEAD.
         }
-      }
+        try {
+          Ref config = db.getRef(RefNames.REFS_CONFIG);
+          if (config != null) {
+            refs.add(config);
+          }
+        } catch (IOException e) {
+          // Ignore the failure reading refs/meta/config.
+        }
 
-      for (final Ref ref : all.values()) {
+      for (Ref ref : refs) {
         if (ref.isSymbolic()) {
           targets.add(ref.getTarget().getName());
         }
       }
 
-      for (final Ref ref : all.values()) {
+      for (Ref ref : refs) {
         if (ref.isSymbolic()) {
           // A symbolic reference to another branch, instead of
           // showing the resolved value, show the name it references.
@@ -122,10 +126,10 @@
 
         final RefControl refControl = rsrc.getControl().controlForRef(ref.getName());
         if (refControl.isVisible()) {
-          if (ref.getName().startsWith(Constants.R_HEADS)) {
-            branches.add(createBranchInfo(ref, refControl, targets));
-          } else if (RefNames.REFS_CONFIG.equals(ref.getName())) {
+          if (RefNames.REFS_CONFIG.equals(ref.getName())) {
             configBranch = createBranchInfo(ref, refControl, targets);
+          } else {
+            branches.add(createBranchInfo(ref, refControl, targets));
           }
         }
       }