33
votes

I am looking to get a list of all of the groups that a user is a member of in Active Directory, both explicitly listed in the memberOf property list as well as implicitly through nested group membership. For example, if I examine UserA and UserA is a part of GroupA and GroupB, I also want to list GroupC if GroupB is a member of GroupC.

To give you a bit more insight into my application, I will be doing this on a limited basis. Basically, I want a security check occasionally that will list these additional memberships. I will want to differentiate the two but that shouldn't be hard.

My problem is that I have not found an efficient way to make this query work. The standard text on Active Directory (This CodeProject Article) shows a way to do this that is basically a recursive lookup. That seems terribly inefficient. Even in my small domain, a user might have 30+ group memberships. That means 30+ calls to Active Directory for one user.

I've looked into the following LDAP code to get all of the memberOf entries at once:

(memberOf:1.2.840.113556.1.4.1941:={0})

where {0} would be my LDAP path (ex: CN=UserA,OU=Users,DC=foo,DC=org). However, it does not return any records. The downside of this method, even if it worked, would be that I wouldn't know which group was explicit and which was implicit.

That is what I have so far. I would like to know if there is a better way than the CodeProject article and, if so, how that could be accomplished (actual code would be wonderful). I am using .NET 4.0 and C#. My Active Directory is at a Windows 2008 functional level (it isn't R2 yet).

6

6 Answers

25
votes

Thirst thanks for this an interesting question.

Next, just a correction, you say :

I've looked into the following LDAP code to get all of the memberOf entries at once:

(memberOf:1.2.840.113556.1.4.1941:={0})

You don't make it work. I remember I make it work when I learnt about its existence, but it was in an LDIFDE.EXE filter. So I apply it to ADSI in C# and it's still working. There were too much parenthesis in the sample I took from Microsoft, but it was working (source in AD Search Filter Syntax).

According to your remark concerning the fact that we don't know if a user explicitly belongs to the group I add one more request. I know this is not very good, but it's the best I'am abable to do.

static void Main(string[] args)
{
  /* Connection to Active Directory
   */
  DirectoryEntry deBase = new DirectoryEntry("LDAP://WM2008R2ENT:389/dc=dom,dc=fr");


  /* To find all the groups that "user1" is a member of :
   * Set the base to the groups container DN; for example root DN (dc=dom,dc=fr) 
   * Set the scope to subtree
   * Use the following filter :
   * (member:1.2.840.113556.1.4.1941:=cn=user1,cn=users,DC=x)
   */
  DirectorySearcher dsLookFor = new DirectorySearcher(deBase);
  dsLookFor.Filter = "(member:1.2.840.113556.1.4.1941:=CN=user1 Users,OU=MonOu,DC=dom,DC=fr)";
  dsLookFor.SearchScope = SearchScope.Subtree;
  dsLookFor.PropertiesToLoad.Add("cn");

  SearchResultCollection srcGroups = dsLookFor.FindAll();

  /* Just to know if user is explicitly in group
   */
  foreach (SearchResult srcGroup in srcGroups)
  {
    Console.WriteLine("{0}", srcGroup.Path);

    foreach (string property in srcGroup.Properties.PropertyNames)
    {
      Console.WriteLine("\t{0} : {1} ", property, srcGroup.Properties[property][0]);
    }

    DirectoryEntry aGroup = new DirectoryEntry(srcGroup.Path);
    DirectorySearcher dsLookForAMermber = new DirectorySearcher(aGroup);
    dsLookForAMermber.Filter = "(member=CN=user1 Users,OU=MonOu,DC=dom,DC=fr)";
    dsLookForAMermber.SearchScope = SearchScope.Base;
    dsLookForAMermber.PropertiesToLoad.Add("cn");

    SearchResultCollection memberInGroup = dsLookForAMermber.FindAll();
    Console.WriteLine("Find the user {0}", memberInGroup.Count);

  }

  Console.ReadLine();
}

In my test tree this give :

LDAP://WM2008R2ENT:389/CN=MonGrpSec,OU=MonOu,DC=dom,DC=fr
adspath : LDAP://WM2008R2ENT:389/CN=MonGrpSec,OU=MonOu,DC=dom,DC=fr
cn : MonGrpSec
Find the user 1

LDAP://WM2008R2ENT:389/CN=MonGrpDis,OU=ForUser1,DC=dom,DC=fr
adspath : LDAP://WM2008R2ENT:389/CN=MonGrpDis,OU=ForUser1,DC=dom,DC=fr
cn : MonGrpDis
Find the user 1

LDAP://WM2008R2ENT:389/CN=MonGrpPlusSec,OU=ForUser1,DC=dom,DC=fr
adspath : LDAP://WM2008R2ENT:389/CN=MonGrpPlusSec,OU=ForUser1,DC=dom,DC=fr
cn : MonGrpPlusSec
Find the user 0

LDAP://WM2008R2ENT:389/CN=MonGrpPlusSecUniv,OU=ForUser1,DC=dom,DC=fr
adspath : LDAP://WM2008R2ENT:389/CN=MonGrpPlusSecUniv,OU=ForUser1,DC=dom,DC=fr
cn : MonGrpPlusSecUniv
Find the user 0

(edited) '1.2.840.113556.1.4.1941' is not working in W2K3 SP1, it begins to work with SP2. I presume it's the same with W2K3 R2. It's supposed to work on W2K8. I test here with W2K8R2. I'll soon be able to test this on W2K8.

7
votes

If there is no way other than recursive calls (and I don't believe there is) then at least you can let the framework do the work for you: see the UserPrincipal.GetAuthorizationGroups method (in the System.DirectoryServices.AccountManagement namespace and introduced in .Net 3.5)

This method searches all groups recursively and returns the groups in which the user is a member. The returned set may also include additional groups that system would consider the user a member of for authorization purposes.

Compare with the results of GetGroups ("Returns a collection of group objects that specify the groups of which the current principal is a member") to see whether the membership is explicit or implicit.

2
votes

Use the ldap filter recursively but query for all groups returned after each query to reduce the number of round trips.

Ex:

  1. Get all groups where user is a member
  2. Get all groups where Step 1 Groups are members
  3. Get all groups where Step 2 Groups are members
  4. ...

In my experience there are rarely more then 5 but should definitiely be much less then 30.

Also:

  • Make sure to only pull the properties you are going to need back.
  • Caching results can significantly aid performance but made my code much more complicated.
  • Make sure to utilize connection pooling.
  • Primary group has to be handled seperately
2
votes

you can utilize the tokenGroups and tokenGroupsGlobalAndUniversal properties if you are on Exchange server. tokenGroups will give you all the security groups this user belongs to, including nested groups and domain users, users, etc tokenGroupsGlobalAndUniversal will include everything from tokenGroups AND distribution groups

private void DoWorkWithUserGroups(string domain, string user)
    {
        var groupType = "tokenGroupsGlobalAndUniversal"; // use tokenGroups for only security groups

        using (var userContext = new PrincipalContext(ContextType.Domain, domain))
        {
            using (var identity = UserPrincipal.FindByIdentity(userContext, IdentityType.SamAccountName, user))
            {
                if (identity == null)
                    return;

                var userEntry = identity.GetUnderlyingObject() as DirectoryEntry;
                userEntry.RefreshCache(new[] { groupType });
                var sids = from byte[] sid in userEntry.Properties[groupType]
                           select new SecurityIdentifier(sid, 0);

                foreach (var sid in sids)
                {
                    using(var groupIdentity = GroupPrincipal.FindByIdentity(userContext, IdentityType.Sid, sid.ToString()))
                    {
                        if(groupIdentity == null)
                            continue; // this group is not in the domain, probably from sidhistory

                        // extract the info you want from the group
                    }
                }
            }
        }
    }
1
votes

If you are using .NET 3.5 or higher you can use the System.DirectoryServices.AccountManagement namespace which really makes this easy.

See the related answer here: Active Directory nested groups

0
votes
    static List<SearchResult> ad_find_all_members(string a_sSearchRoot, string a_sGroupDN, string[] a_asPropsToLoad)
    {
        using (DirectoryEntry de = new DirectoryEntry(a_sSearchRoot))
            return ad_find_all_members(de, a_sGroupDN, a_asPropsToLoad);
    }

    static List<SearchResult> ad_find_all_members(DirectoryEntry a_SearchRoot, string a_sGroupDN, string[] a_asPropsToLoad)
    {
        string sDN = "distinguishedName";
        string sOC = "objectClass";
        string sOC_GROUP = "group";
        string[] asPropsToLoad = a_asPropsToLoad;
        Array.Sort<string>(asPropsToLoad);
        if (Array.BinarySearch<string>(asPropsToLoad, sDN) < 0)
        {
            Array.Resize<string>(ref asPropsToLoad, asPropsToLoad.Length+1);
            asPropsToLoad[asPropsToLoad.Length-1] = sDN;
        }
        if (Array.BinarySearch<string>(asPropsToLoad, sOC) < 0)
        {
            Array.Resize<string>(ref asPropsToLoad, asPropsToLoad.Length+1);
            asPropsToLoad[asPropsToLoad.Length-1] = sOC;
        }

        List<SearchResult> lsr = new List<SearchResult>();

        using (DirectorySearcher ds = new DirectorySearcher(a_SearchRoot))
        {
            ds.Filter = "(&(|(objectClass=group)(objectClass=user))(memberOf=" + a_sGroupDN + "))";
            ds.PropertiesToLoad.Clear();
            ds.PropertiesToLoad.AddRange(asPropsToLoad);
            ds.PageSize = 1000;
            ds.SizeLimit = 0;
            foreach (SearchResult sr in ds.FindAll())
                lsr.Add(sr);
        }

        for(int i=0;i<lsr.Count;i++)
            if (lsr[i].Properties.Contains(sOC) && lsr[i].Properties[sOC].Contains(sOC_GROUP))
                lsr.AddRange(ad_find_all_members(a_SearchRoot, (string)lsr[i].Properties[sDN][0], asPropsToLoad));

        return lsr;
    }

    static void Main(string[] args)
    {
    foreach (var sr in ad_find_all_members("LDAP://DC=your-domain,DC=com", "CN=your-group-name,OU=your-group-ou,DC=your-domain,DC=com", new string[] { "sAMAccountName" }))
        Console.WriteLine((string)sr.Properties["distinguishedName"][0] + " : " + (string)sr.Properties["sAMAccountName"][0]);
    }