Add support for user/group search rules & baseDNs

This is for feature request b/8757112.
diff --git a/src/com/google/enterprise/adaptor/ad/AdAdaptor.java b/src/com/google/enterprise/adaptor/ad/AdAdaptor.java
index 3590e91..234bbd4 100644
--- a/src/com/google/enterprise/adaptor/ad/AdAdaptor.java
+++ b/src/com/google/enterprise/adaptor/ad/AdAdaptor.java
@@ -69,6 +69,10 @@
   private boolean feedBuiltinGroups;
   private GroupCatalog lastCompleteGroupCatalog = null;
   private String ldapTimeoutInMillis;
+  private String userSearchBaseDN;
+  private String groupSearchBaseDN;
+  private String userSearchFilter;
+  private String groupSearchFilter;
 
   @Override
   public void initConfig(Config config) {
@@ -83,6 +87,10 @@
     config.addKey("ad.localized.Builtin", "BUILTIN");
     config.addKey("ad.feedBuiltinGroups", "false");
     config.addKey("ad.ldapReadTimeoutSecs", "90");
+    config.addKey("ad.userSearchBaseDN", "");
+    config.addKey("ad.groupSearchBaseDN", "");
+    config.addKey("ad.userSearchFilter", "");
+    config.addKey("ad.groupSearchFilter", "");
   }
 
   @Override
@@ -97,6 +105,12 @@
         config.getValue("ad.feedBuiltinGroups"));
     ldapTimeoutInMillis = parseLdapTimeoutInMillis(
         config.getValue("ad.ldapReadTimeoutSecs"));
+    // TBD(myk): Determine if any of the following need any sort of validation
+    // beyond the single warning logged if any are provided.
+    userSearchBaseDN = config.getValue("ad.userSearchBaseDN");
+    groupSearchBaseDN = config.getValue("ad.groupSearchBaseDN");
+    userSearchFilter = config.getValue("ad.userSearchFilter");
+    groupSearchFilter = config.getValue("ad.groupSearchFilter");
     // register for incremental pushes
     context.setPollingIncrementalLister(this);
     List<Map<String, String>> serverConfigs
@@ -213,12 +227,14 @@
   @VisibleForTesting
   GroupCatalog makeFullCatalog() throws InterruptedException, IOException {
     GroupCatalog cumulativeCatalog = new GroupCatalog(localizedStrings,
-        namespace, feedBuiltinGroups);
+        namespace, feedBuiltinGroups, userSearchBaseDN, groupSearchBaseDN,
+        userSearchFilter, groupSearchFilter);
     for (AdServer server : servers) {
       try {
         server.ensureConnectionIsCurrent();
         GroupCatalog catalog = new GroupCatalog(localizedStrings, namespace,
-              feedBuiltinGroups);
+              feedBuiltinGroups, userSearchBaseDN, groupSearchBaseDN,
+              userSearchFilter, groupSearchFilter);
         catalog.readEverythingFrom(server, /*includeMembers=*/ true);
         cumulativeCatalog.add(catalog);
       } catch (NamingException ne) {
@@ -240,8 +256,8 @@
   public void getModifiedDocIds(DocIdPusher pusher) throws InterruptedException,
       IOException {
     if (!mutex.tryLock()) {
-      log.log(Level.FINE, "getModifiedDocIds could not acquire lock; " +
-          "will retry later.");
+      log.log(Level.FINE, "getModifiedDocIds could not acquire lock; "
+          + "will retry later.");
       return;
     }
     try {
@@ -314,12 +330,22 @@
     final AdEntity interactive;
     final AdEntity authenticatedUsers;
     final Map<AdEntity, Set<String>> wellKnownMembership;
+    final String userSearchBaseDN;
+    final String groupSearchBaseDN;
+    final String userSearchFilter;
+    final String groupSearchFilter;
 
     public GroupCatalog(Map<String, String> localizedStrings, String namespace,
-        boolean feedBuiltinGroups) {
+        boolean feedBuiltinGroups, String userSearchBaseDN,
+        String groupSearchBaseDN, String userSearchFilter,
+        String groupSearchFilter) {
       this.localizedStrings = localizedStrings;
       this.namespace = namespace;
       this.feedBuiltinGroups = feedBuiltinGroups;
+      this.userSearchBaseDN = userSearchBaseDN;
+      this.groupSearchBaseDN = groupSearchBaseDN;
+      this.userSearchFilter = userSearchFilter;
+      this.groupSearchFilter = groupSearchFilter;
       everyone = new AdEntity("S-1-1-0",
           MessageFormat.format("CN={0}",
           localizedStrings.get("Everyone")));
@@ -366,8 +392,15 @@
         Map<AdEntity, Set<String>> members,
         Map<String, AdEntity> bySid,
         Map<String, AdEntity> byDn,
-        Map<AdEntity, String> domain) {
-      this(localizedStrings, namespace, feedBuiltinGroups);
+        Map<AdEntity, String> domain,
+        String userSearchBaseDN,
+        String groupSearchBaseDN,
+        String userSearchFilter,
+        String groupSearchFilter) {
+      this(localizedStrings, namespace, feedBuiltinGroups, userSearchBaseDN,
+          groupSearchBaseDN, userSearchFilter, groupSearchFilter);
+      this.localizedStrings = localizedStrings;
+      this.namespace = namespace;
       this.entities.clear();
       this.entities.addAll(entities);
       this.members.putAll(members);
@@ -386,19 +419,80 @@
           nonMemberAttributes.length + 1);
       allAttributes[nonMemberAttributes.length] = "member";
       log.log(Level.FINE, "Starting full crawl.");
-      // LDAP_MATCHING_RULE_BIT_AND = 1.2.840.113556.1.4.803
-      // and ADS_GROUP_TYPE_SECURITY_ENABLED = 2147483648.
-      entities = server.search("(|(&(objectClass=group)"
-          + "(groupType:1.2.840.113556.1.4.803:=2147483648))"
-          + "(&(objectClass=user)(objectCategory=person)))",
-          /*deleted=*/ false,
-          includeMembers ? allAttributes : nonMemberAttributes);
+      if (groupSearchBaseDN.equals(userSearchBaseDN)) {
+        entities = server.search(userSearchBaseDN, generateLdapQuery(),
+            /*deleted=*/ false,
+            includeMembers ? allAttributes : nonMemberAttributes);
+      } else {
+        entities = server.search(groupSearchBaseDN, generateGroupLdapQuery(),
+            /*deleted=*/ false,
+            includeMembers ? allAttributes : nonMemberAttributes);
+        entities.addAll(server.search(userSearchBaseDN, generateUserLdapQuery(),
+            /*deleted=*/ false, nonMemberAttributes));
+      }
       // disabled groups handled later, in makeDefs()
       log.log(Level.FINE, "Ending full crawl - now starting processing.");
       processEntities(entities, server.getnETBIOSName());
     }
 
     /**
+     * Generates a query to return groups (optionally with a group filter).
+     * This is useful when either a user BaseDN or a group BaseDN has been
+     * specified (or if they are different).
+     */
+    @VisibleForTesting
+    String generateGroupLdapQuery() {
+      String groupQuery;
+      if ("".equals(groupSearchFilter)) {
+        groupQuery = "(&(objectClass=group)"
+            + "(groupType:1.2.840.113556.1.4.803:=2147483648))";
+        // LDAP_MATCHING_RULE_BIT_AND = 1.2.840.113556.1.4.803
+        // and ADS_GROUP_TYPE_SECURITY_ENABLED = 2147483648.
+      } else {
+        groupQuery = "(&(&(objectClass=group)"
+            + "(groupType:1.2.840.113556.1.4.803:=2147483648))"
+            + "(" + groupSearchFilter + "))";
+      }
+      return groupQuery;
+    }
+
+    /**
+     * Generates a query to return users (optionally with a user filter).
+     * This is useful when either a user BaseDN or a group BaseDN has been
+     * specified (or if they are different).
+     */
+    @VisibleForTesting
+    String generateUserLdapQuery() {
+      String userQuery;
+      if ("".equals(userSearchFilter)) {
+        userQuery = "(&(objectClass=user)(objectCategory=person))";
+      } else {
+        userQuery = "(&(&(objectClass=user)(objectCategory=person))"
+            + "(" + userSearchFilter + "))";
+      }
+      return userQuery;
+    }
+
+    /**
+     * Generates a query to return users (optionally with a user filter) and
+     * groups (optionally with a group filter) -- this is only useful when
+     * neither a user BaseDN or a group BaseDN has been specified (or if they
+     * are both equal).
+     */
+    @VisibleForTesting
+    String generateLdapQuery() {
+      String groupQuery = generateGroupLdapQuery();
+      String userQuery = generateUserLdapQuery();
+      // error if BaseDNs are not equal
+      if (groupSearchBaseDN != userSearchBaseDN) {
+        throw new IllegalArgumentException("not handling differing "
+            + "BaseDNs properly!");
+      }
+      String query = "(|" + groupQuery + userQuery + ")";
+      return query;
+    }
+
+    /**
      * Do an AD search for only groups/users that have been updated since the
      * previous full or incremental search.
      * <p>If either <code>getDsServiceName()</code> or
@@ -459,6 +553,13 @@
     }
 
     private void processEntities(Set<AdEntity> entities, String nETBIOSName) {
+      if (!(("".equals(userSearchBaseDN)) && ("".equals(groupSearchBaseDN))
+          && ("".equals(userSearchFilter)) && ("".equals(groupSearchFilter)))) {
+        log.log(Level.CONFIG, "CAUTION: Customized LDAP search base(s) and/or "
+            + "filter(s) have been configured! If users are experiencing issues"
+            + " with finding content, investigate if relevant users/groups are "
+            + "being excluded from indexing.");
+      }
       log.log(Level.FINE, "received {0} entities from server", entities.size());
       for (AdEntity e : entities) {
         bySid.put(e.getSid(), e);
@@ -476,16 +577,25 @@
     Set<AdEntity> incrementalCrawl(AdServer server, long previousHighestUSN,
         long currentHighestUSN) throws InterruptedNamingException {
       log.log(Level.FINE, "Starting incremental crawl.");
-      // LDAP_MATCHING_RULE_BIT_AND = 1.2.840.113556.1.4.803
-      // and ADS_GROUP_TYPE_SECURITY_ENABLED = 2147483648.
-      Set<AdEntity> newOrModifiedEntities = server.search("(&(uSNChanged>="
-          + (previousHighestUSN + 1) + ")(|(&(objectClass=group)"
-          + "(groupType:1.2.840.113556.1.4.803:=2147483648))"
-          + "(&(objectClass=user)(objectCategory=person))))",
-          /*deleted=*/ false,
-          new String[] { "uSNChanged", "sAMAccountName", "objectGUID;binary",
-              "objectSid;binary", "userPrincipalName", "primaryGroupId",
-              "member", "userAccountControl" });
+      final String[] attributes = new String[] { "uSNChanged", "member",
+          "sAMAccountName", "objectGUID;binary", "objectSid;binary",
+          "userPrincipalName", "primaryGroupId", "userAccountControl" };
+      Set<AdEntity> newOrModifiedEntities;
+
+      String newEntryQuery = "(uSNChanged>=" + (previousHighestUSN + 1) + ")";
+      if (groupSearchBaseDN.equals(userSearchBaseDN)) {
+        newOrModifiedEntities = server.search(userSearchBaseDN,
+            "(&" + newEntryQuery + generateLdapQuery() + ")",
+            /*deleted=*/ false, attributes);
+      } else {
+        newOrModifiedEntities = server.search(groupSearchBaseDN,
+            "(&" + newEntryQuery + generateGroupLdapQuery() + ")",
+            /*deleted=*/ false, attributes);
+        newOrModifiedEntities.addAll(server.search(userSearchBaseDN,
+            "(&" + newEntryQuery + generateUserLdapQuery() + ")",
+            /*deleted=*/ false, attributes));
+      }
+
       // disabled groups handled later, in makeDefs()
       log.log(Level.FINE, "Ending incremental crawl - now starting "
           + "processing.");
diff --git a/src/com/google/enterprise/adaptor/ad/AdServer.java b/src/com/google/enterprise/adaptor/ad/AdServer.java
index de93e5b..a0e7342 100644
--- a/src/com/google/enterprise/adaptor/ad/AdServer.java
+++ b/src/com/google/enterprise/adaptor/ad/AdServer.java
@@ -306,21 +306,25 @@
 
   /**
    * Searches Active Directory and creates AdEntity on each result found
+   * @param baseDN baseDN for the search (use "dn" when empty/null)
    * @param filter LDAP filter to search in the AD for
    * @param attributes list of attributes to retrieve
    * @return list of entities found
    */
-  public Set<AdEntity> search(String filter, boolean deleted,
+  public Set<AdEntity> search(String baseDN, String filter, boolean deleted,
       String[] attributes) throws InterruptedNamingException {
     Set<AdEntity> results = new HashSet<AdEntity>();
     searchCtls.setReturningAttributes(attributes);
     setControls(deleted);
+    if (null == baseDN || "".equals(baseDN)) {
+      baseDN = dn;
+    }
     try {
       ensureConnectionIsCurrent();
       byte[] cookie = null;
       do {
         NamingEnumeration<SearchResult> ldapResults =
-            ldapContext.search(dn, filter, searchCtls);
+            ldapContext.search(baseDN, filter, searchCtls);
         while (ldapResults.hasMoreElements()) {
           SearchResult sr = ldapResults.next();
           try {
@@ -365,7 +369,8 @@
               "Retrieving additional groups for [" + g + "] " + memberRange);
           searchCtls.setReturningAttributes(new String[] {memberRange});
           NamingEnumeration<SearchResult> ldapResults = ldapContext.search(
-              dn, "(sAMAccountName=" + g.getSAMAccountName() + ")", searchCtls);
+              baseDN, "(sAMAccountName=" + g.getSAMAccountName() + ")",
+              searchCtls);
           SearchResult sr = ldapResults.next();
           int found = g.appendGroups(sr);
           start += found;
diff --git a/test/com/google/enterprise/adaptor/ad/AdAdaptorTest.java b/test/com/google/enterprise/adaptor/ad/AdAdaptorTest.java
index a00b22b..dbaa7d0 100644
--- a/test/com/google/enterprise/adaptor/ad/AdAdaptorTest.java
+++ b/test/com/google/enterprise/adaptor/ad/AdAdaptorTest.java
@@ -66,27 +66,21 @@
 
   @Test
   public void testGroupCatalogConstructor() {
-    Map<String, String> strings = defaultLocalizedStringMap();
-
-    AdAdaptor.GroupCatalog groupCatalog = new AdAdaptor.GroupCatalog(
-      strings, "example.com", /*feedBuiltinGroups=*/ false);
-    final AdAdaptor.GroupCatalog golden = new AdAdaptor.GroupCatalog(
-      strings, "example.com", /*feedBuiltinGroups=*/ true);
+    AdAdaptor.GroupCatalog groupCatalog = new GroupCatalogBuilder().build();
+    AdAdaptor.GroupCatalog golden = new GroupCatalogBuilder()
+        .setFeedBuiltinGroups(true).build();
     assertTrue(golden.equals(groupCatalog));
   }
 
   @Test
   public void testGroupCatalogEquals() {
-    Map<String, String> strings = defaultLocalizedStringMap();
-    final AdAdaptor.GroupCatalog golden = new AdAdaptor.GroupCatalog(
-      strings, "example.com", /*feedBuiltinGroups=*/ true);
+    AdAdaptor.GroupCatalog golden = new GroupCatalogBuilder().build();
     golden.members.put(new AdEntity("Test", "dn=Test"),
         Sets.newHashSet("Test"));
     final Map<AdEntity, Set<String>> goldenWellKnownMembership =
         golden.wellKnownMembership;
 
-    AdAdaptor.GroupCatalog groupCatalog = new AdAdaptor.GroupCatalog(
-      strings, "example.com", /*feedBuiltinGroups=*/ false);
+    AdAdaptor.GroupCatalog groupCatalog = new GroupCatalogBuilder().build();
     groupCatalog.members.put(new AdEntity("Test", "dn=Test"),
         Sets.newHashSet("Test"));
 
@@ -133,10 +127,7 @@
 
   @Test
   public void testGroupCatalogReadFrom() throws Exception {
-    Map<String, String> strings = defaultLocalizedStringMap();
-
-    AdAdaptor.GroupCatalog groupCatalog = new AdAdaptor.GroupCatalog(
-      strings, "example.com", /*feedBuiltinGroups=*/ false);
+    AdAdaptor.GroupCatalog groupCatalog = new GroupCatalogBuilder().build();
     MockLdapContext ldapContext = defaultMockLdapContext();
     // add a group
     String filter = "(|(&(objectClass=group)"
@@ -173,23 +164,20 @@
     final Map<AdEntity, String> goldenDomain = new HashMap<AdEntity, String>();
     goldenDomain.put(goldenEntity, "GSA-CONNECTORS");
 
-    final AdAdaptor.GroupCatalog golden = new AdAdaptor.GroupCatalog(
-      strings, "example.com", /*feedBuiltinGroups=*/ true,
-      /*entities*/ Sets.newHashSet(goldenEntity),
-      /*members*/ goldenMembers,
-      /*bySid*/ goldenSid,
-      /*byDn*/ goldenDn,
-      /*domain*/ goldenDomain);
+    AdAdaptor.GroupCatalog golden = new GroupCatalogBuilder()
+        .setFeedBuiltinGroups(true)
+        .setEntities(Sets.newHashSet(goldenEntity))
+        .setMembers(goldenMembers)
+        .setBySid(goldenSid)
+        .setByDn(goldenDn)
+        .setDomain(goldenDomain).build();
 
     assertTrue(golden.equals(groupCatalog));
   }
 
   @Test
   public void testGroupCatalogReadFromReturnsDisabledGroup() throws Exception {
-    Map<String, String> strings = defaultLocalizedStringMap();
-
-    AdAdaptor.GroupCatalog groupCatalog = new AdAdaptor.GroupCatalog(
-      strings, "example.com", /*feedBuiltinGroups=*/ false);
+    AdAdaptor.GroupCatalog groupCatalog = new GroupCatalogBuilder().build();
     MockLdapContext ldapContext = defaultMockLdapContext();
     // add a disabled group
     String filter = "(|(&(objectClass=group)"
@@ -237,21 +225,20 @@
     goldenDomain.put(interactive, "NT Authority");
     goldenDomain.put(authUsers, "NT Authority");
 
-    final AdAdaptor.GroupCatalog golden = new AdAdaptor.GroupCatalog(
-      strings, "example.com", /*feedBuiltinGroups=*/ true,
-      /*entities*/ Sets.newHashSet(goldenEntity),
-      /*members*/ goldenMembers,
-      /*bySid*/ goldenSid,
-      /*byDn*/ goldenDn,
-      /*domain*/ goldenDomain);
+    AdAdaptor.GroupCatalog golden = new GroupCatalogBuilder()
+        .setFeedBuiltinGroups(true)
+        .setEntities(Sets.newHashSet(goldenEntity))
+        .setMembers(goldenMembers)
+        .setBySid(goldenSid)
+        .setByDn(goldenDn)
+        .setDomain(goldenDomain).build();
 
     assertTrue(golden.equals(groupCatalog));
   }
 
   @Test
   public void testGroupCatalogReadFromReturnsUser() throws Exception {
-    AdAdaptor.GroupCatalog groupCatalog = new AdAdaptor.GroupCatalog(
-        defaultLocalizedStringMap(), "example", /*feedBuiltinGroups=*/ false);
+    AdAdaptor.GroupCatalog groupCatalog = new GroupCatalogBuilder().build();
     MockLdapContext ldapContext = defaultMockLdapContext();
     // add a user
     String filter = "(|(&(objectClass=group)"
@@ -288,13 +275,13 @@
     final Map<AdEntity, String> goldenDomain = new HashMap<AdEntity, String>();
     goldenDomain.put(goldenEntity, "BUILTIN");
 
-    final AdAdaptor.GroupCatalog golden = new AdAdaptor.GroupCatalog(
-      defaultLocalizedStringMap(), "example.com", /*feedBuiltinGroups=*/ true,
-      /*entities*/ Sets.newHashSet(goldenEntity, userGroup, everyone),
-      /*members*/ goldenMembers,
-      /*bySid*/ goldenSid,
-      /*byDn*/ goldenDn,
-      /*domain*/ goldenDomain);
+    AdAdaptor.GroupCatalog golden = new GroupCatalogBuilder()
+        .setFeedBuiltinGroups(true)
+        .setEntities(Sets.newHashSet(goldenEntity, userGroup, everyone))
+        .setMembers(goldenMembers)
+        .setBySid(goldenSid)
+        .setByDn(goldenDn)
+        .setDomain(goldenDomain).build();
     golden.wellKnownMembership.get(golden.everyone).add(goldenEntity.getDn());
     assertTrue(golden.equals(groupCatalog));
 
@@ -306,8 +293,7 @@
   @Test
   public void testGroupCatalogReadFromReturnsUserMissingPrimaryGroup()
       throws Exception {
-    AdAdaptor.GroupCatalog groupCatalog = new AdAdaptor.GroupCatalog(
-        defaultLocalizedStringMap(), "example", /*feedBuiltinGroups=*/ false);
+    AdAdaptor.GroupCatalog groupCatalog = new GroupCatalogBuilder().build();
     MockLdapContext ldapContext = defaultMockLdapContext();
     // add a user
     String filter = "(|(&(objectClass=group)"
@@ -339,14 +325,13 @@
     final Map<AdEntity, String> goldenDomain = new HashMap<AdEntity, String>();
     goldenDomain.put(goldenEntity, "BUILTIN");
 
-    final AdAdaptor.GroupCatalog golden = new AdAdaptor.GroupCatalog(
-      defaultLocalizedStringMap(), "example.com", /*feedBuiltinGroups=*/ true,
-      /*entities*/ Sets.newHashSet(goldenEntity),
-      /*members*/ goldenMembers,
-      /*bySid*/ goldenSid,
-      /*byDn*/ goldenDn,
-      /*domain*/ goldenDomain);
-
+    final AdAdaptor.GroupCatalog golden = new GroupCatalogBuilder()
+        .setFeedBuiltinGroups(true)
+        .setEntities(Sets.newHashSet(goldenEntity))
+        .setMembers(goldenMembers)
+        .setBySid(goldenSid)
+        .setByDn(goldenDn)
+        .setDomain(goldenDomain).build();
     assertTrue(golden.equals(groupCatalog));
 
     // make sure readEverythingFrom call is idempotent
@@ -355,10 +340,147 @@
   }
 
   @Test
+  public void testLdapQueriesWithNoFiltersOrBaseDns() throws Exception {
+    final FakeAdaptor adAdaptor = new FakeAdaptor();
+    final FakeCatalog groupCatalog = new FakeCatalog(
+        defaultLocalizedStringMap(), "example.com", false, "", "", "", "");
+    MockLdapContext ldapContext = defaultMockLdapContext();
+    final AdServer adServer = new AdServer("localhost", ldapContext);
+    adServer.initialize();
+    final String expectedGroupQuery = "(&(objectClass=group)"
+        + "(groupType:1.2.840.113556.1.4.803:=2147483648))";
+    assertEquals(expectedGroupQuery, groupCatalog.generateGroupLdapQuery());
+    final String expectedUserQuery = "(&(objectClass=user)"
+        + "(objectCategory=person))";
+    assertEquals(expectedUserQuery, groupCatalog.generateUserLdapQuery());
+    final String expectedQuery = "(|" + expectedGroupQuery + expectedUserQuery
+        + ")";
+    assertEquals(expectedQuery, groupCatalog.generateLdapQuery());
+
+    groupCatalog.resetCrawlFlags();
+    assertFalse(groupCatalog.ranFullCrawl());
+    assertFalse(groupCatalog.ranIncrementalCrawl());
+
+    groupCatalog.readEverythingFrom(adServer, /*includeMembers=*/ true);
+    assertTrue(groupCatalog.ranFullCrawl());
+    assertFalse(groupCatalog.ranIncrementalCrawl());
+
+    groupCatalog.resetCrawlFlags();
+    groupCatalog.readUpdatesFrom(adServer, "ds_service_name", "0x0123456789abc",
+        12345677L); // earlier USN than previous run: does an incremental run
+    assertFalse(groupCatalog.ranFullCrawl());
+    assertTrue(groupCatalog.ranIncrementalCrawl());
+  }
+
+  @Test
+  public void testLdapQueriesWithFilters() throws Exception {
+    final FakeAdaptor adAdaptor = new FakeAdaptor();
+    final FakeCatalog groupCatalog = new FakeCatalog(
+        defaultLocalizedStringMap(), "example.com", false, "", "",
+        "ou=UserFilter", "ou=GroupFilter");
+    MockLdapContext ldapContext = defaultMockLdapContext();
+    final AdServer adServer = new AdServer("localhost", ldapContext);
+    adServer.initialize();
+    final String expectedGroupQuery = "(&(&(objectClass=group)"
+        + "(groupType:1.2.840.113556.1.4.803:=2147483648))(ou=GroupFilter))";
+    assertEquals(expectedGroupQuery, groupCatalog.generateGroupLdapQuery());
+    final String expectedUserQuery = "(&(&(objectClass=user)"
+        + "(objectCategory=person))(ou=UserFilter))";
+    assertEquals(expectedUserQuery, groupCatalog.generateUserLdapQuery());
+    final String expectedQuery = "(|" + expectedGroupQuery + expectedUserQuery
+        + ")";
+    assertEquals(expectedQuery, groupCatalog.generateLdapQuery());
+
+    groupCatalog.resetCrawlFlags();
+    assertFalse(groupCatalog.ranFullCrawl());
+    assertFalse(groupCatalog.ranIncrementalCrawl());
+
+    groupCatalog.readEverythingFrom(adServer, /*includeMembers=*/ true);
+    assertTrue(groupCatalog.ranFullCrawl());
+    assertFalse(groupCatalog.ranIncrementalCrawl());
+
+    groupCatalog.resetCrawlFlags();
+    groupCatalog.readUpdatesFrom(adServer, "ds_service_name", "0x0123456789abc",
+        12345677L); // earlier USN than previous run: does an incremental run
+    assertFalse(groupCatalog.ranFullCrawl());
+    assertTrue(groupCatalog.ranIncrementalCrawl());
+  }
+
+  @Test
+  public void testLdapQueriesWithBaseDNsButNoFilters() throws Exception {
+    final FakeAdaptor adAdaptor = new FakeAdaptor();
+    final FakeCatalog groupCatalog = new FakeCatalog(
+        defaultLocalizedStringMap(), "example.com", false, "ou=UserBaseDn",
+        "ou=GroupBaseDn", "", "");
+    MockLdapContext ldapContext = defaultMockLdapContext();
+    final AdServer adServer = new AdServer("localhost", ldapContext);
+    adServer.initialize();
+    final String expectedGroupQuery = "(&(objectClass=group)"
+        + "(groupType:1.2.840.113556.1.4.803:=2147483648))";
+    assertEquals(expectedGroupQuery, groupCatalog.generateGroupLdapQuery());
+    final String expectedUserQuery = "(&(objectClass=user)"
+        + "(objectCategory=person))";
+    assertEquals(expectedUserQuery, groupCatalog.generateUserLdapQuery());
+    final String expectedQuery = "(|" + expectedGroupQuery + expectedUserQuery
+        + ")";
+    try {
+      String query = groupCatalog.generateLdapQuery();
+      fail("Did not catch expected exception!");
+    } catch (IllegalArgumentException iae) {
+      assertTrue(iae.toString().contains("not handling differing BaseDNs"));
+    }
+
+    groupCatalog.resetCrawlFlags();
+    assertFalse(groupCatalog.ranFullCrawl());
+    assertFalse(groupCatalog.ranIncrementalCrawl());
+
+    groupCatalog.readEverythingFrom(adServer, /*includeMembers=*/ true);
+    assertTrue(groupCatalog.ranFullCrawl());
+    assertFalse(groupCatalog.ranIncrementalCrawl());
+
+    groupCatalog.resetCrawlFlags();
+    groupCatalog.readUpdatesFrom(adServer, "ds_service_name", "0x0123456789abc",
+        12345677L); // earlier USN than previous run: does an incremental run
+    assertFalse(groupCatalog.ranFullCrawl());
+    assertTrue(groupCatalog.ranIncrementalCrawl());
+  }
+
+  @Test
+  public void testLdapQueriesWithBaseDNsAndFilters() throws Exception {
+    final FakeAdaptor adAdaptor = new FakeAdaptor();
+    AdAdaptor.GroupCatalog groupCatalog = new GroupCatalogBuilder()
+        .setUserSearchBaseDN("ou=UserBaseDn")
+        .setGroupSearchBaseDN("ou=GroupBaseDn")
+        .setUserSearchFilter("ou=UserFilter")
+        .setGroupSearchFilter("ou=GroupFilter").build();
+    MockLdapContext ldapContext = defaultMockLdapContext();
+    final AdServer adServer = new AdServer("localhost", ldapContext);
+    adServer.initialize();
+    final String expectedGroupQuery = "(&(&(objectClass=group)"
+        + "(groupType:1.2.840.113556.1.4.803:=2147483648))(ou=GroupFilter))";
+    assertEquals(expectedGroupQuery, groupCatalog.generateGroupLdapQuery());
+    final String expectedUserQuery = "(&(&(objectClass=user)"
+        + "(objectCategory=person))(ou=UserFilter))";
+    assertEquals(expectedUserQuery, groupCatalog.generateUserLdapQuery());
+    final String expectedQuery = "(|" + expectedGroupQuery + expectedUserQuery
+        + ")";
+    try {
+      String query = groupCatalog.generateLdapQuery();
+      fail("Did not catch expected exception!");
+    } catch (IllegalArgumentException iae) {
+      assertTrue(iae.toString().contains("not handling differing BaseDNs"));
+    }
+
+    groupCatalog.readEverythingFrom(adServer, /*includeMembers=*/ false);
+    groupCatalog.readUpdatesFrom(adServer, "ds_service_name", "0x0123456789abc",
+        12345677L); // earlier USN than previous run: does an incremental run
+  }
+
+  @Test
   public void testFullCrawlVersusIncrementalCrawlFlow() throws Exception {
     final FakeAdaptor adAdaptor = new FakeAdaptor();
     final FakeCatalog groupCatalog = new FakeCatalog(
-        defaultLocalizedStringMap(), "example.com", false);
+        defaultLocalizedStringMap(), "example.com", false, "", "", "", "");
     MockLdapContext ldapContext = defaultMockLdapContext();
     final AdServer adServer = new AdServer("localhost", ldapContext);
     adServer.initialize();
@@ -431,10 +553,7 @@
 
   @Test
   public void testGroupCatalogReadFromIncrementalCrawl() throws Exception {
-    Map<String, String> strings = defaultLocalizedStringMap();
-
-    AdAdaptor.GroupCatalog groupCatalog = new AdAdaptor.GroupCatalog(
-      strings, "example.com", /*feedBuiltinGroups=*/ false);
+    AdAdaptor.GroupCatalog groupCatalog = new GroupCatalogBuilder().build();
     MockLdapContext ldapContext = defaultMockLdapContext();
     // add a group
     String filter = "(|(&(objectClass=group)"
@@ -485,8 +604,8 @@
         "0x0123456789abc", 12345677L);
 
     // extract incrementally-added user as one golden entity
-    Set<AdEntity> incrementalUserSet = adServer.search(incrementalFilter, false,
-        new String[] { "member", "objectSid;binary", "objectGUID;binary",
+    Set<AdEntity> incrementalUserSet = adServer.search("", incrementalFilter,
+        false, new String[] { "member", "objectSid;binary", "objectGUID;binary",
             "primaryGroupId", "sAMAccountName" });
     goldenResults = incrementalUserSet;
     assertEquals(goldenResults, updateResults);
@@ -499,7 +618,7 @@
     final AdEntity goldenUserEntity = searchedEntity[0];
 
     // extract group from full crawl as the other golden entity
-    Set<AdEntity> fullyCrawledGroupSet = adServer.search(filter, false,
+    Set<AdEntity> fullyCrawledGroupSet = adServer.search("", filter, false,
         new String[] { "member", "objectSid;binary", "objectGUID;binary",
             "primaryGroupId", "sAMAccountName" });
     assertEquals(1, fullyCrawledGroupSet.size());
@@ -524,13 +643,13 @@
     goldenDomain.put(goldenUserEntity, "BUILTIN");
     goldenDomain.put(goldenGroupEntity, "GSA-CONNECTORS");
 
-    final AdAdaptor.GroupCatalog golden = new AdAdaptor.GroupCatalog(
-      strings, "example.com", /*feedBuiltinGroups=*/ true,
-      /*entities*/ Sets.newHashSet(goldenUserEntity, goldenGroupEntity),
-      /*members*/ goldenMembers,
-      /*bySid*/ goldenSid,
-      /*byDn*/ goldenDn,
-      /*domain*/ goldenDomain);
+    final AdAdaptor.GroupCatalog golden = new GroupCatalogBuilder()
+        .setFeedBuiltinGroups(true)
+        .setEntities(Sets.newHashSet(goldenUserEntity, goldenGroupEntity))
+        .setMembers(goldenMembers)
+        .setBySid(goldenSid)
+        .setByDn(goldenDn)
+        .setDomain(goldenDomain).build();
     assertEquals(golden, groupCatalog);
 
     // do another incremental crawl with same results
@@ -542,10 +661,7 @@
   @Test
   public void testGroupCatalogResolveForeignSecurityPrincipals()
       throws Exception {
-    Map<String, String> strings = defaultLocalizedStringMap();
-
-    AdAdaptor.GroupCatalog groupCatalog = new AdAdaptor.GroupCatalog(
-      strings, "example.com", /*feedBuiltinGroups=*/ false);
+    AdAdaptor.GroupCatalog groupCatalog = new GroupCatalogBuilder().build();
     MockLdapContext ldapContext = defaultMockLdapContext();
     // add a group
     String filter = "(|(&(objectClass=group)"
@@ -580,7 +696,7 @@
 
     // add two additional entities to test all branches of our method.
     // first -- a user
-    Set<AdEntity> userEntitySet = adServer.search(filter2, false,
+    Set<AdEntity> userEntitySet = adServer.search("", filter2, false,
         new String[] { "cn", "objectSid;binary", "objectGUID;binary",
             "primaryGroupId", "sAMAccountName" });
     assertEquals(1, userEntitySet.size());
@@ -598,7 +714,7 @@
     groupCatalog.resolveForeignSecurityPrincipals(groupCatalog.entities);
 
     // extract original group entity
-    Set<AdEntity> groupEntitySet = adServer.search(filter, false,
+    Set<AdEntity> groupEntitySet = adServer.search("", filter, false,
         new String[] { "member", "objectSid;binary", "objectGUID;binary",
             "sAMAccountName" });
     assertEquals(1, groupEntitySet.size());
@@ -631,15 +747,14 @@
     goldenDn.put(goldenEntity.getDn(), goldenEntity);
     final Map<AdEntity, String> goldenDomain = new HashMap<AdEntity, String>();
     goldenDomain.put(goldenEntity, "GSA-CONNECTORS");
-    final AdAdaptor.GroupCatalog golden = new AdAdaptor.GroupCatalog(
-      defaultLocalizedStringMap(), "example.com", /*feedBuiltinGroups=*/ true,
-      /*entities*/ goldenEntities,
-      /*members*/ goldenMembers,
-      /*bySid*/ goldenSid,
-      /*byDn*/ goldenDn,
-      /*domain*/ goldenDomain);
-
-    assertTrue(golden.equals(groupCatalog));
+    final AdAdaptor.GroupCatalog golden = new GroupCatalogBuilder()
+        .setFeedBuiltinGroups(true)
+        .setEntities(goldenEntities)
+        .setMembers(goldenMembers)
+        .setBySid(goldenSid)
+        .setByDn(goldenDn)
+        .setDomain(goldenDomain).build();
+    assertEquals(golden, groupCatalog);
 
     // make sure resolveForeignSecurityPrincipals call is idempotent
     groupCatalog.resolveForeignSecurityPrincipals(groupCatalog.entities);
@@ -648,10 +763,7 @@
 
   @Test
   public void testGroupCatalogMakeDefs() throws Exception {
-    Map<String, String> strings = defaultLocalizedStringMap();
-
-    AdAdaptor.GroupCatalog groupCatalog = new AdAdaptor.GroupCatalog(
-      strings, "example.com", /*feedBuiltinGroups=*/ false);
+    AdAdaptor.GroupCatalog groupCatalog = new GroupCatalogBuilder().build();
 
     MockLdapContext ldapContext = mockLdapContextForMakeDefs(false);
 
@@ -677,10 +789,7 @@
 
   @Test
   public void testGroupCatalogMakeDefsWithDisabledGroup() throws Exception {
-    Map<String, String> strings = defaultLocalizedStringMap();
-
-    AdAdaptor.GroupCatalog groupCatalog = new AdAdaptor.GroupCatalog(
-      strings, "example.com", /*feedBuiltinGroups=*/ false);
+    AdAdaptor.GroupCatalog groupCatalog = new GroupCatalogBuilder().build();
 
     MockLdapContext ldapContext = mockLdapContextForMakeDefs(true);
 
@@ -704,10 +813,8 @@
 
   @Test
   public void testGroupCatalogMakeDefsWellKnownParent() throws Exception {
-    Map<String, String> strings = defaultLocalizedStringMap();
-
-    AdAdaptor.GroupCatalog groupCatalog = new AdAdaptor.GroupCatalog(
-      strings, "example.com", /*feedBuiltinGroups=*/ true);
+    AdAdaptor.GroupCatalog groupCatalog = new GroupCatalogBuilder()
+        .setFeedBuiltinGroups(true).build();
     Logger log = Logger.getLogger(AdAdaptor.class.getName());
     Level oldLevel = log.getLevel();
     log.setLevel(Level.FINER);
@@ -746,7 +853,7 @@
         replacementGroup.getMembers().add("dn=");
         groupCatalog.members.put(groupWithNoName, new TreeSet<String>());
 
-        Set<AdEntity> emptyUser = adServer.search(filter, false,
+        Set<AdEntity> emptyUser = adServer.search("", filter, false,
         new String[] { "objectSid;binary", "objectGUID;binary",
                        "primaryGroupId", "sAMAccountName" });
         assertEquals(1, emptyUser.size());
@@ -807,6 +914,7 @@
     configEntries.put("ad.defaultUser", "defaultUser");
     configEntries.put("ad.defaultPassword", "password");
     configEntries.put("ad.ldapReadTimeoutSecs", "");
+    configEntries.put("ad.userSearchFilter", "cn=UserNotFound");
     configEntries.put("server.port", "5680");
     configEntries.put("server.dashboardPort", "5681");
     pushGroupDefinitions(adAdaptor, configEntries, pusher, /*fullPush=*/ true,
@@ -833,6 +941,50 @@
     configEntries.put("ad.defaultUser", "defaultUser");
     configEntries.put("ad.defaultPassword", "password");
     configEntries.put("ad.ldapReadTimeoutSecs", "0");
+    configEntries.put("ad.groupSearchFilter", "cn=GroupNotFound");
+    configEntries.put("server.port", "5680");
+    configEntries.put("server.dashboardPort", "5681");
+    pushGroupDefinitions(adAdaptor, configEntries, pusher, /*fullPush=*/ true,
+        /*init=*/ true);
+    Map<GroupPrincipal, Collection<Principal>> results = pusher.getGroups();
+    // the above (eventually) calls AdAdaptor.init() with the specified config.
+  }
+
+  @Test
+  public void testFakeAdaptorUserAndPasswordSpecified() throws Exception {
+    AdAdaptor adAdaptor = new FakeAdaptor();
+    AccumulatingDocIdPusher pusher = new AccumulatingDocIdPusher();
+    Map<String, String> configEntries = new HashMap<String, String>();
+    configEntries.put("gsa.hostname", "localhost");
+    configEntries.put("ad.servers", "server1");
+    configEntries.put("ad.servers.server1.host", "localhost");
+    configEntries.put("ad.servers.server1.port", "1234");
+    configEntries.put("ad.servers.server1.user", "username");
+    configEntries.put("ad.servers.server1.password", "password");
+    configEntries.put("ad.servers.server1.method", "ssl");
+    configEntries.put("ad.userSearchBaseDN", "ou=DoesNotMatter");
+    configEntries.put("server.port", "5680");
+    configEntries.put("server.dashboardPort", "5681");
+    pushGroupDefinitions(adAdaptor, configEntries, pusher, /*fullPush=*/ true,
+        /*init=*/ true);
+    Map<GroupPrincipal, Collection<Principal>> results = pusher.getGroups();
+    // the above (eventually) calls AdAdaptor.init() with the specified config.
+  }
+
+  @Test
+  public void testFakeAdaptorDefaultUserAndPasswordSpecified()
+      throws Exception {
+    AdAdaptor adAdaptor = new FakeAdaptor();
+    AccumulatingDocIdPusher pusher = new AccumulatingDocIdPusher();
+    Map<String, String> configEntries = new HashMap<String, String>();
+    configEntries.put("gsa.hostname", "localhost");
+    configEntries.put("ad.servers", "server1");
+    configEntries.put("ad.servers.server1.host", "localhost");
+    configEntries.put("ad.servers.server1.port", "1234");
+    configEntries.put("ad.servers.server1.method", "ssl");
+    configEntries.put("ad.defaultUser", "defaultUser");
+    configEntries.put("ad.defaultPassword", "defaultPassword");
+    configEntries.put("ad.groupSearchBaseDN", "ou=DoesNotMatter");
     configEntries.put("server.port", "5680");
     configEntries.put("server.dashboardPort", "5681");
     pushGroupDefinitions(adAdaptor, configEntries, pusher, /*fullPush=*/ true,
@@ -978,12 +1130,13 @@
           int timesSearchCalled = 0;
           int timesEnsureConnectionCalled = 0;
           @Override
-          public Set<AdEntity> search(String filter, boolean deleted,
-              String[] attributes) throws InterruptedNamingException {
+          public Set<AdEntity> search(String baseDn, String filter,
+              boolean deleted, String[] attributes)
+              throws InterruptedNamingException {
             if (errorFilter.equals(filter) && timesSearchCalled++ == 0) {
               throw new InterruptedNamingException("First exception");
             } else {
-              return super.search(filter, deleted, attributes);
+              return super.search(baseDn, filter, deleted, attributes);
             }
           }
           @Override
@@ -1039,7 +1192,7 @@
     return AdServerTest.hexStringToByteArray(s);
   }
 
-  private Map<String, String> defaultLocalizedStringMap() {
+  private static Map<String, String> defaultLocalizedStringMap() {
     Map<String, String> strings = new HashMap<String, String>();
     strings.put("Everyone", "everyone");
     strings.put("NTAuthority", "NT Authority");
@@ -1126,7 +1279,7 @@
     // add two additional entities to test all branches of our method.
     assertEquals(1, groupCatalog.entities.size());
     // first -- a user
-    Set<AdEntity> userEntity = adServer.search(
+    Set<AdEntity> userEntity = adServer.search("",
         "(&(objectClass=user)(objectCategory=person))", false,
         new String[] { "cn", "objectSid;binary", "objectGUID;binary",
             "primaryGroupId", "sAMAccountName" });
@@ -1320,8 +1473,11 @@
     private boolean ranIncrementalCrawl;
 
     public FakeCatalog(Map<String, String> localizedStrings, String namespace,
-        boolean feedBuiltinGroups) {
-      super(localizedStrings, namespace, feedBuiltinGroups);
+        boolean feedBuiltinGroups, String userSearchBaseDn,
+        String groupSearchBaseDn, String userSearchFilter,
+        String groupSearchFilter) {
+      super(localizedStrings, namespace, feedBuiltinGroups, userSearchBaseDn,
+          groupSearchBaseDn, userSearchFilter, groupSearchFilter);
     }
 
     @Override
@@ -1395,7 +1551,7 @@
       }
       ranFullCrawl = true;
       return new AdAdaptor.GroupCatalog(defaultLocalizedStringMap(),
-          "example.com", /*feedBuiltinGroups=*/ true);
+          "example.com", /*feedBuiltinGroups=*/ true, "", "", "", "");
     }
   };
 
@@ -1432,4 +1588,105 @@
       }));
     }
   }
+
+  private static class GroupCatalogBuilder {
+    private Map<String, String> localizedStrings = defaultLocalizedStringMap();
+    private String namespace = "example.com";
+    private boolean feedBuiltinGroups = false;
+    private String userSearchBaseDN = "";
+    private String groupSearchBaseDN = "";
+    private String userSearchFilter = "";
+    private String groupSearchFilter = "";
+    private Set<AdEntity> entities;
+    private Map<AdEntity, Set<String>> members;
+    private Map<String, AdEntity> bySid;
+    private Map<String, AdEntity> byDn;
+    private Map<AdEntity, String> domain;
+    /**
+     * The following field is only used by this Builder class, to determine if
+     * the build() method should call the standard constructor or the extended
+     * constructor.
+     */
+    private boolean useExtendedConstructor = false;
+
+    public GroupCatalogBuilder setLocalizedStrings(Map<String, String> locals) {
+      this.localizedStrings = locals;
+      return this;
+    }
+
+    public GroupCatalogBuilder setNamespace(String namespace) {
+      this.namespace = namespace;
+      return this;
+    }
+
+    public GroupCatalogBuilder setFeedBuiltinGroups(boolean feedBuiltinGroups) {
+      this.feedBuiltinGroups = feedBuiltinGroups;
+      return this;
+    }
+
+    public GroupCatalogBuilder setUserSearchBaseDN(String userSearchBaseDN) {
+      this.userSearchBaseDN = userSearchBaseDN;
+      return this;
+    }
+
+    public GroupCatalogBuilder setGroupSearchBaseDN(String groupSearchBaseDN) {
+      this.groupSearchBaseDN = groupSearchBaseDN;
+      return this;
+    }
+
+    public GroupCatalogBuilder setUserSearchFilter(String userSearchFilter) {
+      this.userSearchFilter = userSearchFilter;
+      return this;
+    }
+
+    public GroupCatalogBuilder setGroupSearchFilter(String groupSearchFilter) {
+      this.groupSearchFilter = groupSearchFilter;
+      return this;
+    }
+
+    // The remaining fields are the "extended" fields -- their setters set the
+    // variable as well as the flag to use the extended constructor.
+
+    public GroupCatalogBuilder setEntities(Set<AdEntity> entities) {
+      this.entities = entities;
+      this.useExtendedConstructor = true;
+      return this;
+    }
+
+    public GroupCatalogBuilder setMembers(Map<AdEntity, Set<String>> members) {
+      this.members = members;
+      this.useExtendedConstructor = true;
+      return this;
+    }
+
+    public GroupCatalogBuilder setBySid(Map<String, AdEntity> bySid) {
+      this.bySid = bySid;
+      this.useExtendedConstructor = true;
+      return this;
+    }
+
+    public GroupCatalogBuilder setByDn(Map<String, AdEntity> byDn) {
+      this.byDn = byDn;
+      this.useExtendedConstructor = true;
+      return this;
+    }
+
+    public GroupCatalogBuilder setDomain(Map<AdEntity, String> domain) {
+      this.domain = domain;
+      this.useExtendedConstructor = true;
+      return this;
+    }
+
+    public AdAdaptor.GroupCatalog build() {
+      if (useExtendedConstructor) {
+        return new AdAdaptor.GroupCatalog(localizedStrings, namespace,
+            feedBuiltinGroups, entities, members, bySid, byDn, domain,
+            userSearchBaseDN, groupSearchBaseDN, userSearchFilter,
+            groupSearchFilter);
+      }
+      return new AdAdaptor.GroupCatalog(localizedStrings, namespace,
+          feedBuiltinGroups, userSearchBaseDN, groupSearchBaseDN,
+          userSearchFilter, groupSearchFilter);
+    }
+  }
 }
diff --git a/test/com/google/enterprise/adaptor/ad/AdServerTest.java b/test/com/google/enterprise/adaptor/ad/AdServerTest.java
index 66973d1..2a66305 100644
--- a/test/com/google/enterprise/adaptor/ad/AdServerTest.java
+++ b/test/com/google/enterprise/adaptor/ad/AdServerTest.java
@@ -16,19 +16,18 @@
 
 import static org.junit.Assert.*;
 
+import com.google.enterprise.adaptor.InvalidConfigurationException;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 
-import com.google.enterprise.adaptor.InvalidConfigurationException;
-import com.google.enterprise.adaptor.StartupException;
+import java.util.*;
 
 import javax.naming.*;
 import javax.naming.directory.*;
 import javax.naming.ldap.*;
 
-import java.util.*;
-
 /** Test cases for {@link AdServer}. */
 public class AdServerTest {
   @Rule
@@ -307,7 +306,49 @@
                    hexStringToByteArray("000102030405060708090a0b0c"));
     AdServer adServer = new AdServer("localhost", ldapContext);
     adServer.initialize();
-    Set<AdEntity> resultSet = adServer.search(filter, false,
+    Set<AdEntity> resultSet = adServer.search("", filter, false,
+        new String[] { "cn", "primaryGroupId", "objectGUID;binary" });
+    assertEquals(1, resultSet.size());
+    for (AdEntity ae : resultSet) {
+      assertEquals("name under", ae.getCommonName());
+    }
+  }
+
+  @Test
+  public void testSearchWithNullBaseDnReturnsOneUser() throws Exception {
+    MockLdapContext ldapContext = new MockLdapContext();
+    addStandardKeysAndResults(ldapContext);
+    // populate additional attributes with values we can test
+    final String filter = "ou=Users";
+    final String userDn = "DN_for_default_naming_context";
+    ldapContext.addSearchResult(filter, "cn", userDn, "user1")
+               .addSearchResult(filter, "primaryGroupId", userDn, "users")
+               .addSearchResult(filter, "objectGUID;binary", userDn,
+                   hexStringToByteArray("000102030405060708090a0b0c"));
+    AdServer adServer = new AdServer("localhost", ldapContext);
+    adServer.initialize();
+    Set<AdEntity> resultSet = adServer.search(null, filter, false,
+        new String[] { "cn", "primaryGroupId", "objectGUID;binary" });
+    assertEquals(1, resultSet.size());
+    for (AdEntity ae : resultSet) {
+      assertEquals("name under", ae.getCommonName());
+    }
+  }
+
+  @Test
+  public void testSearchUnderDifferentBaseDnReturnsOneUser() throws Exception {
+    MockLdapContext ldapContext = new MockLdapContext();
+    addStandardKeysAndResults(ldapContext);
+    // populate additional attributes with values we can test
+    final String filter = "ou=Users";
+    final String userDn = "DN_for_default_naming_context";
+    ldapContext.addSearchResult(filter, "cn", userDn, "user1")
+               .addSearchResult(filter, "primaryGroupId", userDn, "users")
+               .addSearchResult(filter, "objectGUID;binary", userDn,
+                   hexStringToByteArray("000102030405060708090a0b0c"));
+    AdServer adServer = new AdServer("localhost", ldapContext);
+    adServer.initialize();
+    Set<AdEntity> resultSet = adServer.search(userDn, filter, false,
         new String[] { "cn", "primaryGroupId", "objectGUID;binary" });
     assertEquals(1, resultSet.size());
     for (AdEntity ae : resultSet) {
@@ -326,7 +367,7 @@
                .addSearchResult(filter, "primaryGroupId", userDn, "users");
     AdServer adServer = new AdServer("localhost", ldapContext);
     adServer.initialize();
-    Set<AdEntity> resultSet = adServer.search(filter, false,
+    Set<AdEntity> resultSet = adServer.search("", filter, false,
         new String[] { "cn", "primaryGroupId", "objectGUID;binary" });
     assertEquals(0, resultSet.size());
   }
@@ -349,7 +390,7 @@
                .addSearchResult(filter, "primaryGroupId", userDn, "users");
     AdServer adServer = new AdServer("localhost", ldapContext);
     adServer.initialize();
-    Set<AdEntity> resultSet = adServer.search(filter, false,
+    Set<AdEntity> resultSet = adServer.search("", filter, false,
         new String[] { "cn", "primaryGroupId", "objectGUID;binary" });
     assertEquals(0, resultSet.size());
   }
@@ -367,7 +408,7 @@
                    hexStringToByteArray("000102030405060708090a0b0c"));
     AdServer adServer = new AdServer("localhost", ldapContext);
     adServer.initialize();
-    Set<AdEntity> resultSet = adServer.search(filter, true,
+    Set<AdEntity> resultSet = adServer.search("", filter, true,
         new String[] { "cn", "primaryGroupId", "objectGUID;binary" });
     assertEquals(1, resultSet.size());
     for (AdEntity ae : resultSet) {
@@ -393,7 +434,7 @@
                    hexStringToByteArray("000102030405060708090a0b0c"));
     AdServer adServer = new AdServer("localhost", ldapContext);
     adServer.initialize();
-    Set<AdEntity> resultSet = adServer.search(filter, true,
+    Set<AdEntity> resultSet = adServer.search("", filter, true,
         new String[] { "cn", "members", "objectGUID;binary" });
     assertEquals(1, resultSet.size());
     for (AdEntity ae : resultSet) {
@@ -408,14 +449,25 @@
     // populate additional attributes with values we can test
     final String filter = "ou=Users";
     final String userDn = "DN_for_default_naming_context";
+    final String group2Dn = "cn=Cn_for_another_group";
     List<String> members = Arrays.asList("dn_for_user_1", "dn_for_user_2");
     ldapContext.addSearchResult(filter, "cn", userDn, "users")
                .addSearchResult(filter, "objectGUID;binary", userDn,
                    hexStringToByteArray("000102030405060708090a0b0c"))
-               .addSearchResult(filter, "member", userDn, members);
+               .addSearchResult(filter, "member", userDn, members)
+               .addSearchResult(filter, "objectGUID;binary", group2Dn,
+                   hexStringToByteArray("000102030405060708090a0b0d"))
+               .addSearchResult(filter, "member", group2Dn, members);
     AdServer adServer = new AdServer("localhost", ldapContext);
     adServer.initialize();
-    Set<AdEntity> resultSet = adServer.search(filter, false,
+    Set<AdEntity> resultSet = adServer.search("", filter, false,
+        new String[] { "cn", "member", "objectGUID;binary" });
+    assertEquals(1, resultSet.size());
+    for (AdEntity ae : resultSet) {
+      assertEquals(new HashSet<String>(members), ae.getMembers());
+    }
+    // read second group under its baseDn
+    resultSet = adServer.search(group2Dn, filter, false,
         new String[] { "cn", "member", "objectGUID;binary" });
     assertEquals(1, resultSet.size());
     for (AdEntity ae : resultSet) {
@@ -446,7 +498,7 @@
                     moreMembers);
     AdServer adServer = new AdServer("localhost", ldapContext);
     adServer.initialize();
-    Set<AdEntity> resultSet = adServer.search(filter, false,
+    Set<AdEntity> resultSet = adServer.search("", filter, false,
         // need the ranged members for MockLdapContext, not for "real" AD.
         new String[] { "cn", "member", "member;Range=0-1", "member;Range=2-3",
                        "objectGUID;binary", "sAMAccountName" });
@@ -484,7 +536,7 @@
                     evenMore);
     AdServer adServer = new AdServer("localhost", ldapContext);
     adServer.initialize();
-    Set<AdEntity> resultSet = adServer.search(filter, false,
+    Set<AdEntity> resultSet = adServer.search("", filter, false,
         // need the ranged members for MockLdapContext, not for "real" AD.
         new String[] { "cn", "member", "member;Range=0-1", "member;Range=2-3",
                        "member;Range=4-5*", "objectGUID;binary",