views:

1574

answers:

3

I created a WCF web service to return user and group information from Active Directory. It works fine for most groups and users, but not all.

I use directoryEntry.Invoke("groups",null) to return the groups a specified user is member of. This returns MOST groups. The odd thing is that I can find any group and enumerate its members, even if it is one of the groups missing when I use the invoke query on one of its members.

Most of the groups that exhibit this behavior are Exchange-enabled. Most of the problematic user accounts are for users in a federated domain, who use an Exchange server in the domain that I query. I am not trying to query objects in the federated domain.

My theories so far:

  • some security restriction does not allow enumerating all groups via invoke() even though I can query missing groups and enumerate their members.

  • invoke has issues with some subset of groups. Perhaps universal, dynamic, or Exchange-enabled properties are at play

  • the invoke method does not pick up all groups because the "federated" accounts (created as part of their Exchange account setup) are somehow different than regular domain accounts beyond the sid mapping back to their login domain.

+1  A: 

There are two known issues with using the "Groups" property on a DirectoryEntry:

  • it will not show you the "Default group" a user is in (typically "Users")
  • it will not show you nested group memberships

So if a user is member of a group A, and that group then in turn is member of Group B, then in Windows, this means that the user is also member of Group B. However, the DirectoryEntry will not show you that nested group membership.

Those are the two only restrictions I know of for straight Active Directory (without Exchange).

Getting the default group is a bit involved, but I do have a code sample for that.

private string GetPrimaryGroup(DirectoryEntry aEntry, DirectoryEntry aDomainEntry)
{
   int primaryGroupID = (int)aEntry.Properties["primaryGroupID"].Value;
   byte[] objectSid = (byte[])aEntry.Properties["objectSid"].Value;

   StringBuilder escapedGroupSid = new StringBuilder();

   // Copy over everything but the last four bytes(sub-authority)
   // Doing so gives us the RID of the domain
   for(uint i = 0; i < objectSid.Length - 4; i++)
   {
        escapedGroupSid.AppendFormat("\\{0:x2}", objectSid[i]);
   }

   //Add the primaryGroupID to the escape string to build the SID of the primaryGroup
   for(uint i = 0; i < 4; i++)
   {
       escapedGroupSid.AppendFormat("\\{0:x2}", (primaryGroupID & 0xFF));
       primaryGroupID >>= 8;
   }

   //Search the directory for a group with this SID
   DirectorySearcher searcher = new DirectorySearcher();
   if(aDomainEntry != null)
   {
      searcher.SearchRoot = aDomainEntry;
   }

   searcher.Filter = "(&(objectCategory=Group)(objectSID=" + escapedGroupSid.ToString() + "))";
   searcher.PropertiesToLoad.Add("distinguishedName");

   return searcher.FindOne().Properties["distinguishedName"][0].ToString();
}

Getting the nested groups also takes a few steps and I'll have to hunt for a solution to that one, if that's the problem.

Marc

PS: as a side note - why on earth are you doing a "DirectoryEntry.Invoke("groups", null)" call? Why don't you just enumerate the DirectoryEntry.Properties["memberOf"] property which is multi-valued (contains multiple values) and has the group's DN (distinguished name) in it?

foreach(string groupDN in myUser.Properties["memberOf"])
{
  string groupName = groupDN;
}

OR if you're on .NET 3.5, you can make use of the new Security Principal classes in S.DS.AccountManagement. One of them is a "UserPrincipal", which has a method called "GetAuthorizationGroups()" which does all this hard work for you - for free, basically!

See an excellent MSDN article that describes these new .NET 3.5 S.DS features for you.

marc_s
These are good suggestions and you are right about the MSDN article, I should have read that before starting. Regarding why I used invoke method, in this instance all I needed was the group names, and that seemed like an efficient way to get the info. I have not done very much programming against Active Directory, so I am still very much learning the best way to proceed. It is interesting how much .NET has evolved in this area since 1.1!
HeathenWorld
No problem at all - just curious if you had a particular reason for using the Invoke() approach. And hey - that's what StackOverflow is all about - helping you when you don't see the forest for the trees :-)
marc_s
+1 for the linked MSDN article, much easier in 3.5
tbone
A: 

I think marc_s is correct. If you want all groups, you can use the following snippet:

using (DirectoryEntry obj = new DirectoryEntry("LDAP://" + dn))
{
 obj.RefreshCache(new string[] { "tokenGroups" });
 string[] sids = new string[obj.Properties["tokenGroups"].Count];
 int i = 0;
 foreach (byte[] bytes in obj.Properties["tokenGroups"])
 {
  sids[i] = _ConvertSidToString(bytes);
  ++i;
 }
 obj.Close();
 return sids;
}

Note that calculating nested groups is an expensive operation, so RefreshCache might take a long time to complete.

On Freund
A: 

On Freund,

I am trying to make use of your code and not getting very far. I have updated the directory entry path to be "LDAP://DC=myDomain,DC=co,DC=uk" but I am not getting any results (obj.Properties["tokenGroups"].Count = 0)

I don't udnerstand how the user to list group for is specified.

Could you please point me in the right direction?

Thanks

EDIT:

I got it sorted in the end. The directory entry to get the token groups from should be a user entry... if that makes sense...

I've included some code in case anyone else has the same query:

Dim directoryEntry As DirectoryEntry = _
      New DirectoryEntry("LDAP://CN=users,DC=domanName,DC=com")
Dim directorySearcher As DirectorySearcher = _
      New DirectorySearcher(directoryEntry, "(sAMAccountName=" & UserName & ")")
Dim searchResult As SearchResult = directorySearcher.FindOne()

If Not searchResult Is Nothing Then
    Dim userDirectoryEntry As DirectoryEntry = searchResult.GetDirectoryEntry
    userDirectoryEntry.RefreshCache(New String() {"tokenGroups"})
    ... etc ...
End If