Home > .net 2.0, .net 3.5, c#, Microsoft, Windows Server > Fetching Nested Group Memberships in Active Directory

Fetching Nested Group Memberships in Active Directory

July 22, 2009

As we’ve started using Active Directory more and more to provide single sign-on services for our web applications, group memberships have become more important.

We recently rolled out an application that took advantage of nesting groups (easier to add and manage five global groups than 10,000 individuals); however, our existing code to fetch memberships wouldn’t look at nested groups.

So if I was a member of “Student Achievement”, how could I parse the memberships of that group and determine if I was in “MIS”?

Thankfully, a bit of recursion does the trick…🙂

As our infrastructure is entirely Windows Server 2003 and higher, I use the System.DirectoryServices.Protocols namespace and methods to connect to and parse out information from LDAP.  Because of this, I rely on SearchResult(s) rather than DirectoryEntries. 

In our environment, a “user” is defined as:

“(&(objectCategory=person)(objectClass=user)(mail=*)({0}={1}))”

Everything looks pretty plain except we require that a valid “user” have an email address.  That ensures we filter out junk/test accounts as only employees have Exchange accounts.

Groups are even easier:

“(objectCategory=group)”

If, say I’ve queried for a single user, the groups property is populated simply by looking at the local user’s “memberOf” attribute.

private static IEnumerable<string> ParseGroupMemberships(SearchResultEntry result, int countOfGroups)

{

    for (int i = 0; i < countOfGroups; i++)

    {

        var fullGroupName = (string) result.Attributes[“memberOf”][i];

        //Fully Qualified Distinguished Name looks like:

        //CN={GroupName},OU={AnOU},DC={domain},DC={suffix}

        //CN=DCI,OU=Groups,OU=Data Center,DC=usd259,DC=net

        int startGroupName = fullGroupName.IndexOf(“=”, 1);

        int endGroupName = fullGroupName.IndexOf(“,”, 1);

        if (startGroupName != -1)

        {

            string friendlyName =

                fullGroupName.Substring(startGroupName + 1, (endGroupNamestartGroupName) – 1);

            yield return friendlyName;

        }

    }

}

That was fine for the primary groups (attached through memberOf); however, it didn’t look at the groups those groups were a “memberOf”.🙂

After quite a bit of trial and error, the new method looks pretty ugly, but seems to be quite performant and reliant in tests. 

private static IEnumerable<string> ParseGroupMemberships(

    SearchResultEntry result, int countOfGroups)

{

    var primaryGroups = new List<string>(countOfGroups);

    var allGroups = new List<string>();

 

    for (int index = 0; index < countOfGroups; index++)

    {

        primaryGroups.Add(result.Attributes[ldapGroupsAttribute][index].ToString());

        allGroups.Add(result.Attributes[ldapGroupsAttribute][index].ToString());

    }

 

    var connection = new ActiveDirectory().GetConnection();

 

    while (0 < primaryGroups.Count)

    {

        var searchRequest = new SearchRequest(distinguishedName,

                                              CreateFilterFromGroups(primaryGroups),

                                              SearchScope.Subtree,

                                              ldapGroupsAttribute);

        primaryGroups.Clear();

 

        var response = (SearchResponse)connection.SendRequest(searchRequest);

        if (response != null)

        {

            int entriesCount = response.Entries.Count;

            for (int entry = 0; entry < entriesCount; entry++)

            {

                DirectoryAttribute groupList =

                    response.Entries[entry].Attributes[ldapGroupsAttribute];

 

                if (groupList != null)

                {

                    int groupCount = groupList.Count;

                    for (int index = 0; index < groupCount; index++)

                    {

                        string dn = groupList[index].ToString();

                        if (!allGroups.Contains(dn))

                        {

                            allGroups.Add(dn);

                            primaryGroups.Add(dn);

                        }

                    }

                }

            }

        }

    }

    connection.Dispose();

 

    foreach (string dn in allGroups)

    {

        yield return GetFriendlyName(dn);

    }

}

Here’s a breakdown of the highlights:

var primaryGroups = new List<string>(countOfGroups);

var allGroups = new List<string>();

 

for (int index = 0; index < countOfGroups; index++)

{

    primaryGroups.Add(result.Attributes[ldapGroupsAttribute][index].ToString());

    allGroups.Add(result.Attributes[ldapGroupsAttribute][index].ToString());

}

This section takes the SearchResultEntry’s primary groups and adds each one of them to two lists.

  • The ‘primaryGroups’ list is exactly that—here’s a list of groups that we need to iterate over and find the nested groups. 
  • The ‘allGroups’ will hold our master list of every unique group and will provide our return value.

var searchRequest = new SearchRequest(distinguishedName,

                                      CreateFilterFromGroups(primaryGroups),

                                      SearchScope.Subtree,

                                      ldapGroupsAttribute);

primaryGroups.Clear();

This code formulates our LDAP search request. distinguishedName and ldapGroupsAttribute are two constants in my code base (for our domain’s DN and “memberOf”).  CreateFilterFromGroups takes the list of groups and concats them together—so we’re only looking at the groups we want, not everything.

Finally, we’re reusing our primaryGroups list to look for nested within nested… within nested, so clear that out—infinite loops hinder performance.🙂

int entriesCount = response.Entries.Count;

for (int entry = 0; entry < entriesCount; entry++)

{

    DirectoryAttribute groupList =

        response.Entries[entry].Attributes[ldapGroupsAttribute];

 

    if (groupList != null)

    {

        int groupCount = groupList.Count;

        for (int index = 0; index < groupCount; index++)

        {

            string dn = groupList[index].ToString();

            if (!allGroups.Contains(dn))

            {

                allGroups.Add(dn);

                primaryGroups.Add(dn);

            }

        }

    }

}

Here’s our massive, disgusting block of if statements that populate the lists and keep the where statement running as long as primaryGroups returns a count > 0.

foreach (string dn in allGroups)

{

    yield return GetFriendlyName(dn);

}

Finally, use a helper method to convert the DN to a “friendly name” and return it to the caller (using yield since our method returns an IEnumerable<string>).

Running a quick test gives me:

UserAccount_Can_Get_Group_Memberships_With_Default_Security : Passed

Group count for David Longnecker is 138
Elapsed time for first query: 00:00:00.0420000

Wow, I’m in a lot of groups…O_o. The query is relatively quick (that is with connection buildup and teardown time and generating the rest of the attributes of the user) especially considering our AD infrastructure is far from optimal.

In addition, a LDAP query using ADUC gives the same results. 

If nothing else, its consistent! :) 

%d bloggers like this: