Merge branch 'master' of https://code.google.com/p/plexi.ad
diff --git a/src/com/google/enterprise/adaptor/ad/AdAdaptor.java b/src/com/google/enterprise/adaptor/ad/AdAdaptor.java
index a0bb712..532170a 100644
--- a/src/com/google/enterprise/adaptor/ad/AdAdaptor.java
+++ b/src/com/google/enterprise/adaptor/ad/AdAdaptor.java
@@ -246,7 +246,8 @@
           /*deleted=*/ false,
           new String[] { "uSNChanged", "sAMAccountName", "objectGUID;binary",
               "objectSid;binary", "userPrincipalName", "primaryGroupId",
-              "member" });
+              "member", "userAccountControl" });
+      // disabled groups handled later, in makeDefs()
       log.log(Level.FINE, "received {0} entities from server", entities.size());
       for (AdEntity e : entities) {
         bySid.put(e.getSid(), e);
@@ -366,6 +367,12 @@
           continue;
         }
 
+        if (entity.isDisabled()) {
+          log.log(Level.FINE, "Skipping {0} members from disabled group {1}",
+              new Object[]{entity.getMembers().size(), group});
+          groups.put(group, def);
+          continue;
+        }
         for (String memberDn : allMembers.get(entity)) {
           AdEntity member = byDn.get(memberDn);
           if (member == null) {
diff --git a/src/com/google/enterprise/adaptor/ad/AdEntity.java b/src/com/google/enterprise/adaptor/ad/AdEntity.java
index 90fce2f..f1e60c2 100644
--- a/src/com/google/enterprise/adaptor/ad/AdEntity.java
+++ b/src/com/google/enterprise/adaptor/ad/AdEntity.java
@@ -1,5 +1,7 @@
 package com.google.enterprise.adaptor.ad;
 
+import com.google.common.annotations.VisibleForTesting;
+
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Set;
@@ -27,6 +29,7 @@
   private long uSNChanged;
   private boolean wellKnown;
   private boolean allMembershipsRetrieved;
+  private long userAccountControl;  // determines whether user/group is disabled
   private final Pattern attrMemberPattern =
       Pattern.compile("member;range=[0-9]+-.*", Pattern.CASE_INSENSITIVE);
 
@@ -78,6 +81,12 @@
     }
     primaryGroupId = (String) getAttribute(attrs, "primaryGroupId");
     userPrincipalName = (String) getAttribute(attrs, "userPrincipalName");
+    s = (String) getAttribute(attrs, "userAccountControl");
+    if (s == null) {
+      userAccountControl = 0; // not disabled - any value where value & 2 == 0
+    } else {
+      userAccountControl = Long.parseLong(s);
+    }
 
     members = new HashSet<String>();
     if (isGroup()) {
@@ -232,6 +241,7 @@
     result.append("objectGUID", objectGUID);
     result.append("sid", sid);
     result.append("uSNChanged", uSNChanged);
+    result.append("userAccountControl", userAccountControl);
     result.append("allMembershipsRetrieved", allMembershipsRetrieved);
     return result.toString();
   }
@@ -262,6 +272,19 @@
   }
 
   /**
+  * @return if current entity is disabled
+  */
+  public boolean isDisabled() {
+    //TODO(myk): see if there's any reason to check ADS_UF_LOCKOUT [16]
+    return ((userAccountControl & 2) != 0);
+  }
+
+  @VisibleForTesting
+  void setUserAccountControl(long userAccountControl) {
+    this.userAccountControl = userAccountControl;
+  }
+
+  /**
    * @return if we need to retrieve further memberships for this group
    */
   public boolean areAllMembershipsRetrieved() {
@@ -299,14 +322,17 @@
     }
     AdEntity other = (AdEntity) o;
     return dn.equals(other.dn)
-        && sAMAccountName.equals(other.sAMAccountName)
+        && ((sAMAccountName == null) ? (other.sAMAccountName == null) :
+            sAMAccountName.equals(other.sAMAccountName))
         && ((userPrincipalName == null) ? (other.userPrincipalName == null) :
             userPrincipalName.equals(other.userPrincipalName))
         && ((primaryGroupId == null) ? (other.primaryGroupId == null) :
             primaryGroupId.equals(other.primaryGroupId))
         && ((sid == null) ? (other.sid == null) : sid.equals(other.sid))
-        && members.equals(other.members)
-        && uSNChanged == other.uSNChanged;
+        && ((members == null) ? (other.members == null) :
+            members.equals(other.members))
+        && uSNChanged == other.uSNChanged
+        && userAccountControl == other.userAccountControl;
         // note: 3 fields (objectGUID, wellKnown, and allMembershipsRetrieved)
         // are intentionally skipped - we'd need a setter method to make the
         // "golden" values correct.
@@ -316,7 +342,7 @@
   public int hashCode() {
     // same 3 fields as above are excluded here.
     return Arrays.hashCode(new Object[] {dn, sAMAccountName, userPrincipalName,
-        primaryGroupId, sid, members, uSNChanged});
+        primaryGroupId, sid, members, uSNChanged, userAccountControl});
   }
 
   /**
diff --git a/test/com/google/enterprise/adaptor/ad/AdAdaptorTest.java b/test/com/google/enterprise/adaptor/ad/AdAdaptorTest.java
index 79d7574..6115d08 100644
--- a/test/com/google/enterprise/adaptor/ad/AdAdaptorTest.java
+++ b/test/com/google/enterprise/adaptor/ad/AdAdaptorTest.java
@@ -160,11 +160,75 @@
   }
 
   @Test
+  public void testGroupCatalogReadFromReturnsDisabledGroup() throws Exception {
+    Map<String, String> strings = defaultLocalizedStringMap();
+
+    AdAdaptor.GroupCatalog groupCatalog = new AdAdaptor.GroupCatalog(
+      strings, "example.com", /*feedBuiltinGroups=*/ false);
+    MockLdapContext ldapContext = defaultMockLdapContext();
+    // add a disabled group
+    String filter = "(|(&(objectClass=group)"
+        + "(groupType:1.2.840.113556.1.4.803:=2147483648))"
+        + "(&(objectClass=user)(objectCategory=person)))";
+    String searchDn = "DN_for_default_naming_context";
+    List<String> members = Arrays.asList("dn_for_user_1", "dn_for_user_2");
+    ldapContext.addSearchResult(filter, "cn", searchDn, "group_name")
+               .addSearchResult(filter, "objectSid;binary", searchDn,
+                   hexStringToByteArray("010100000000000000000000")) // S-1-0-0
+               .addSearchResult(filter, "objectGUID;binary", searchDn,
+                   hexStringToByteArray("000102030405060708090a0b0e"))
+               .addSearchResult(filter, "member", searchDn, members)
+               .addSearchResult(filter, "userAccountControl", searchDn, "514")
+               .addSearchResult(filter, "sAMAccountName", searchDn,
+                   "name under");
+
+    AdServer adServer = new AdServer("localhost", ldapContext);
+    adServer.initialize();
+
+    groupCatalog.readFrom(adServer);
+
+    AdEntity[] groupEntity = groupCatalog.entities.toArray(new AdEntity[0]);
+    final AdEntity goldenEntity = groupEntity[0];
+    final Map<AdEntity, Set<String>> goldenMembers =
+        new HashMap<AdEntity, Set<String>>();
+    goldenMembers.put(goldenEntity, goldenEntity.getMembers());
+    final Map<String, AdEntity> goldenSid =
+        new HashMap<String, AdEntity>();
+    goldenSid.put(goldenEntity.getSid(), goldenEntity);
+    AdEntity everyone = groupCatalog.bySid.get("S-1-1-0");
+    AdEntity authUsers = groupCatalog.bySid.get("S-1-5-11");
+    AdEntity interactive = groupCatalog.bySid.get("S-1-5-4");
+    goldenSid.put("S-1-1-0", everyone);
+    goldenSid.put("S-1-5-11", authUsers);
+    goldenSid.put("S-1-5-4", interactive);
+    final Map<String, AdEntity> goldenDn =
+        new HashMap<String, AdEntity>();
+    goldenDn.put(goldenEntity.getDn(), goldenEntity);
+    goldenDn.put(everyone.getDn(), everyone);
+    goldenDn.put(authUsers.getDn(), authUsers);
+    goldenDn.put(interactive.getDn(), interactive);
+    final Map<AdEntity, String> goldenDomain = new HashMap<AdEntity, String>();
+    goldenDomain.put(goldenEntity, "GSA-CONNECTORS");
+    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);
+
+    assertTrue(golden.equals(groupCatalog));
+  }
+
+  @Test
   public void testGroupCatalogReadFromReturnsUser() throws Exception {
     AdAdaptor.GroupCatalog groupCatalog = new AdAdaptor.GroupCatalog(
         defaultLocalizedStringMap(), "example", /*feedBuiltinGroups=*/ false);
     MockLdapContext ldapContext = defaultMockLdapContext();
-    // add a group
+    // add a user
     String filter = "(|(&(objectClass=group)"
         + "(groupType:1.2.840.113556.1.4.803:=2147483648))"
         + "(&(objectClass=user)(objectCategory=person)))";
@@ -378,14 +442,14 @@
     AdAdaptor.GroupCatalog groupCatalog = new AdAdaptor.GroupCatalog(
       strings, "example.com", /*feedBuiltinGroups=*/ false);
 
-    MockLdapContext ldapContext = mockLdapContextForMakeDefs();
+    MockLdapContext ldapContext = mockLdapContextForMakeDefs(false);
 
     AdServer adServer = new AdServer("localhost", ldapContext);
     adServer.initialize();
 
     groupCatalog.readFrom(adServer);
 
-    tweakGroupCatalogForMakeDefs(groupCatalog, adServer);
+    tweakGroupCatalogForMakeDefs(groupCatalog, adServer, false);
 
     final Map<GroupPrincipal, List<Principal>> golden =
         new HashMap<GroupPrincipal, List<Principal>>();
@@ -401,6 +465,33 @@
   }
 
   @Test
+  public void testGroupCatalogMakeDefsWithDisabledGroup() throws Exception {
+    Map<String, String> strings = defaultLocalizedStringMap();
+
+    AdAdaptor.GroupCatalog groupCatalog = new AdAdaptor.GroupCatalog(
+      strings, "example.com", /*feedBuiltinGroups=*/ false);
+
+    MockLdapContext ldapContext = mockLdapContextForMakeDefs(true);
+
+    AdServer adServer = new AdServer("localhost", ldapContext);
+    adServer.initialize();
+
+    groupCatalog.readFrom(adServer);
+
+    tweakGroupCatalogForMakeDefs(groupCatalog, adServer, true);
+
+    final Map<GroupPrincipal, List<Principal>> golden =
+        new HashMap<GroupPrincipal, List<Principal>>();
+    {
+      golden.put(new GroupPrincipal("sam@GSA-CONNECTORS", "example.com"),
+          Collections.<Principal>emptyList());
+      golden.put(new GroupPrincipal("known_group", "example.com"),
+          Collections.<Principal>emptyList());
+    }
+    assertEquals(golden, groupCatalog.makeDefs());
+  }
+
+  @Test
   public void testGroupCatalogMakeDefsWellKnownParent() throws Exception {
     Map<String, String> strings = defaultLocalizedStringMap();
 
@@ -410,7 +501,7 @@
     Level oldLevel = log.getLevel();
     log.setLevel(Level.FINER);
 
-    MockLdapContext ldapContext = mockLdapContextForMakeDefs();
+    MockLdapContext ldapContext = mockLdapContextForMakeDefs(false);
     String searchDn = "DN_for_default_naming_context";
     String filter = "(objectCategory=person)";
     ldapContext.addSearchResult(filter, "objectSid;binary", searchDn,
@@ -426,7 +517,7 @@
 
     groupCatalog.readFrom(adServer);
 
-    tweakGroupCatalogForMakeDefs(groupCatalog, adServer);
+    tweakGroupCatalogForMakeDefs(groupCatalog, adServer, false);
     // now replace the parent group with a well-known one
     AdEntity replacementGroup = new AdEntity("S-1-0-0", "dn=new_parent");
     AdEntity groupWithNoName = new AdEntity("", "dn=");
@@ -610,7 +701,7 @@
           String principal, String passwd) {
         MockLdapContext ldapContext = null;
         try {
-          ldapContext = mockLdapContextForMakeDefs();
+          ldapContext = mockLdapContextForMakeDefs(false);
         } catch (Exception e) {
           fail("Could not create LdapContext:" + e);
         }
@@ -689,7 +780,8 @@
     return ldapContext;
   }
 
-  private MockLdapContext mockLdapContextForMakeDefs() throws Exception {
+  private MockLdapContext mockLdapContextForMakeDefs(boolean disableSamGroup)
+      throws Exception {
     MockLdapContext ldapContext = defaultMockLdapContext();
     // add a group
     String filter = "(|(&(objectClass=group)"
@@ -706,6 +798,8 @@
                .addSearchResult(filter, "objectGUID;binary", searchDn,
                    hexStringToByteArray("000102030405060708090a0b0e"))
                .addSearchResult(filter, "member", searchDn, members)
+               .addSearchResult(filter, "userAccountControl", searchDn,
+                   (disableSamGroup ? "514" : "512"))
                .addSearchResult(filter, "sAMAccountName", searchDn, "sam");
     // and a user (under another filter)
     String filter2 = "(&(objectClass=user)(objectCategory=person))";
@@ -722,7 +816,7 @@
   }
 
   private void tweakGroupCatalogForMakeDefs(AdAdaptor.GroupCatalog groupCatalog,
-      AdServer adServer) throws Exception {
+      AdServer adServer, boolean disableSamGroup) throws Exception {
     // add two additional entities to test all branches of our method.
     assertEquals(1, groupCatalog.entities.size());
     // first -- a user
@@ -887,7 +981,7 @@
         String principal, String passwd) {
       MockLdapContext ldapContext = null;
       try {
-        ldapContext = mockLdapContextForMakeDefs();
+        ldapContext = mockLdapContextForMakeDefs(false);
       } catch (Exception e) {
         fail("Could not create LdapContext:" + e);
       }
diff --git a/test/com/google/enterprise/adaptor/ad/AdEntityTest.java b/test/com/google/enterprise/adaptor/ad/AdEntityTest.java
index cfca6d4..73256bc 100644
--- a/test/com/google/enterprise/adaptor/ad/AdEntityTest.java
+++ b/test/com/google/enterprise/adaptor/ad/AdEntityTest.java
@@ -33,6 +33,7 @@
         AdServerTest.hexStringToByteArray("010100000000000000000000"));
     attrs.put("uSNChanged", "12345678");
     attrs.put("primaryGroupId", "users");
+    attrs.put("userAccountControl", "512");  // standard, enabled, user
 
     SearchResult sr = new SearchResult("SR name", attrs, attrs);
     sr.setNameInNamespace("cn=user,ou=Users,dc=example,dc=com");
@@ -43,18 +44,62 @@
     assertFalse(adEntity.isWellKnown());
     assertEquals(0, adEntity.getMembers().size());
     assertEquals("S-1-0-users", adEntity.getPrimaryGroupSid());
+    assertFalse(adEntity.isDisabled());
   }
 
   @Test
   public void testWellKnownConstructor() throws Exception {
     AdEntity adEntity = new AdEntity("S-1-1-1",
         "dn=escaped\\,cn=users,ou=Users,dc=example,dc=com");
+    adEntity.setUserAccountControl(514);  // disabled user
     assertEquals("escaped,cn=users", adEntity.getCommonName());
     assertEquals("S-1-1-1", adEntity.getSid());
     assertEquals("dn=escaped\\,cn=users,ou=Users,dc=example,dc=com",
         adEntity.getDn());
     assertTrue(adEntity.isWellKnown());
     assertEquals(0, adEntity.getMembers().size());
+    assertTrue(adEntity.isDisabled());
+  }
+
+  @Test
+  public void testEquals() throws Exception {
+    AdEntity one = one = new AdEntity("foo", "bar");
+    String nonAdEntity = new String("bogus");
+    assertFalse(one.equals(nonAdEntity));
+    AdEntity two = new AdEntity("foo", "baz");
+    assertFalse(one.equals(nonAdEntity));
+    two = new AdEntity("baz", "bar");
+    assertFalse(one.equals(nonAdEntity));
+
+    Attributes attrs = new BasicAttributes();
+    attrs.put("objectGUID;binary",
+        AdServerTest.hexStringToByteArray("000102030405060708090a0b0c"));
+    attrs.put("objectSid;binary", // S-1-0-0
+        AdServerTest.hexStringToByteArray("010100000000000000000000"));
+    attrs.put("uSNChanged", "12345678");
+    attrs.put("primaryGroupId", "users");
+    attrs.put("userPrincipalName", "user");
+    attrs.put("sAMAccountName", "sam");
+    attrs.put("userAccountControl", "512");  // standard, enabled, user
+
+    SearchResult sr = new SearchResult("SR name", attrs, attrs);
+    sr.setNameInNamespace("cn=user,ou=Users,dc=example,dc=com");
+    one = new AdEntity(sr);
+    attrs.put("primaryGroupId", "another group");
+    sr = new SearchResult("SR name", attrs, attrs);
+    sr.setNameInNamespace("cn=user,ou=Users,dc=example,dc=com");
+    two = new AdEntity(sr);
+    assertFalse(one.equals(two));
+    // TODO(myk): additional equality tests for other fields, if deemed useful
+
+    // test userAccountControl field for equality
+    one = new AdEntity("dn1", "dn=user,ou=Users,dc=example,dc=com");
+    two = new AdEntity("dn1", "dn=user,ou=Users,dc=example,dc=com");
+    assertEquals(one, two);
+    two.setUserAccountControl(514);  // disabled user
+    assertFalse(one.equals(two));
+    two.setUserAccountControl(0);
+    assertEquals(one, two);
   }
 
   @Test