Support recursive queries against LDAP directories

The top domain of a company might be DC=example,DC=com, while a
sub-domain may be DC=asia,DC=example,DC=com.  Assuming all users
in the company will use Gerrit, but their accounts are in different
sub-domains, recursive search under DC=example,DC=com is required.
So ldap.accountScope can be set to subtree for this case.

Change-Id: I9c6f98148b00e7c3f1e197054efcb2899f370d74
Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 13384e1..80f5191 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -514,6 +514,17 @@
 Root of the tree containing all user accounts.  This is typically
 of the form `ou=people,dc=example,dc=com`.
 
+[[ldap.accountScope]]ldap.accountScope::
++
+Scope of the search performed for accounts.  Must be one of:
++
+* `one`: Search only one level below accountBase, but not recursive
+* `sub` or `subtree`: Search recursively below accountBase
+* `base` or `object`: Search exactly accountBase; probably not desired
+
++
+Default is `subtree` as many directories have several levels.
+
 [[ldap.accountPattern]]ldap.accountPattern::
 +
 Query pattern to use when searching for a user account.  This may be
@@ -572,6 +583,17 @@
 Root of the tree containing all group objects.  This is typically
 of the form `ou=groups,dc=example,dc=com`.
 
+[[ldap.groupScope]]ldap.groupScope::
++
+Scope of the search performed for group objects.  Must be one of:
++
+* `one`: Search only one level below groupBase, but not recursive
+* `sub` or `subtree`: Search recursively below groupBase
+* `base` or `object`: Search exactly groupBase; probably not desired
+
++
+Default is `subtree` as many directories have several levels.
+
 [[ldap.groupName]]ldap.groupName::
 +
 Name of an attribute on the group object which matches to the name
diff --git a/src/main/java/com/google/gerrit/server/ldap/LdapQuery.java b/src/main/java/com/google/gerrit/server/ldap/LdapQuery.java
index 4a03335..b0eb938 100644
--- a/src/main/java/com/google/gerrit/server/ldap/LdapQuery.java
+++ b/src/main/java/com/google/gerrit/server/ldap/LdapQuery.java
@@ -29,14 +29,45 @@
 
 /** Supports issuing parameterized queries against an LDAP data source. */
 class LdapQuery {
+  static enum SearchScope {
+    // Search only the base DN
+    //
+    OBJECT(SearchControls.OBJECT_SCOPE), //
+    BASE(SearchControls.OBJECT_SCOPE),
+
+    // Search all entries one level under the base DN
+    //
+    // Does not include the base DN, and does not include items below items
+    // under the base DN.
+    //
+    ONE(SearchControls.ONELEVEL_SCOPE),
+
+    // Search all entries under the base DN, including the base DN.
+    //
+    SUBTREE(SearchControls.SUBTREE_SCOPE), //
+    SUB(SearchControls.SUBTREE_SCOPE);
+
+    private final int scope;
+
+    SearchScope(final int scope) {
+      this.scope = scope;
+    }
+
+    int scope() {
+      return scope;
+    }
+  }
+
   private final String base;
+  private final SearchScope searchScope;
   private final String pattern;
   private final String[] patternArgs;
   private final String[] returnAttributes;
 
-  LdapQuery(final String base, final String pattern,
-      final Set<String> returnAttributes) {
+  LdapQuery(final String base, final SearchScope searchScope,
+      final String pattern, final Set<String> returnAttributes) {
     this.base = base;
+    this.searchScope = searchScope;
 
     final StringBuilder p = new StringBuilder();
     final List<String> a = new ArrayList<String>(4);
@@ -76,7 +107,7 @@
     final SearchControls sc = new SearchControls();
     final NamingEnumeration<SearchResult> res;
 
-    sc.setSearchScope(SearchControls.ONELEVEL_SCOPE);
+    sc.setSearchScope(searchScope.scope());
     sc.setReturningAttributes(returnAttributes);
     res = ctx.search(base, pattern, bind(params), sc);
     try {
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 2d0d2a0..e127cd1 100644
--- a/src/main/java/com/google/gerrit/server/ldap/LdapRealm.java
+++ b/src/main/java/com/google/gerrit/server/ldap/LdapRealm.java
@@ -26,7 +26,9 @@
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.SelfPopulatingCache;
+import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.ldap.LdapQuery.SearchScope;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.SchemaFactory;
 import com.google.inject.Inject;
@@ -96,9 +98,11 @@
     groupName = reqdef(config, "groupName", "cn");
     groupAtts.add(groupName);
     final String groupBase = required(config, "groupBase");
+    final SearchScope groupScope = scope(config, "groupScope");
     final String groupMemberPattern =
         reqdef(config, "groupMemberPattern", "(memberUid=${username})");
-    groupMemberQuery = new LdapQuery(groupBase, groupMemberPattern, groupAtts);
+    groupMemberQuery =
+        new LdapQuery(groupBase, groupScope, groupMemberPattern, groupAtts);
     if (groupMemberQuery.getParameters().length == 0) {
       throw new IllegalArgumentException(
           "No variables in ldap.groupMemberPattern");
@@ -140,9 +144,11 @@
       }
     }
     final String accountBase = required(config, "accountBase");
+    final SearchScope accountScope = scope(config, "accountScope");
     final String accountPattern =
         reqdef(config, "accountPattern", "(uid=${username})");
-    accountQuery = new LdapQuery(accountBase, accountPattern, accountAtts);
+    accountQuery =
+        new LdapQuery(accountBase, accountScope, accountPattern, accountAtts);
     if (accountQuery.getParameters().length == 0) {
       throw new IllegalArgumentException("No variables in ldap.accountPattern");
     }
@@ -155,6 +161,10 @@
     };
   }
 
+  private static SearchScope scope(final Config c, final String setting) {
+    return ConfigUtil.getEnum(c, "ldap", null, setting, SearchScope.SUBTREE);
+  }
+
   private static String optional(final Config config, final String name) {
     return config.getString("ldap", null, name);
   }