How to Get All User Members From Nested Security Groups in Active Directory Using .NET and C#

Hello, Randy Evans here. I am a principal developer on the Information Security Tools team.

In a recent project, we found it necessary to get collections of users from security groups defined in Active Directory (AD). It is common practice for security administrators to create hierarchies of security groups, which allows for easier membership management. The requirement we faced was that whatever privileges were granted to members of a security group, those privileges apply to any members of security groups found downstream in the hierarchy. This is also a commonly used concept. It was necessary for use to find the security group in AD (represented as a single directory entry) and then drill down into each level of the group hierarchy and find all user members of all associated security groups.

If you’re at all familiar with AD you’ll know that the members of a group (be it a security group or a distribution group) are contained in a property collection called “members”. It is a simple process to query AD for the group and retrieve the collection of members for that group. The difficulty comes in when the security group contains one or members that are security groups. AD does not store properties in the member collection that indicate the member’s type in the member collection. Thus, it becomes necessary to query AD on every member to get their directory entry. The directory entry contains the metadata required to determine the type of the member.

I’ve provided some simplified sample code that demonstrates our solution. The first object (GetAllUsers) gathers the members into a collection that is passed back to the client. The second is a static object (CISFLDAP) that makes the call to AD to retrieve a specific directory entry and return the entry’s collection of properties to the first object. GetAllUsers has two methods. The client calls the GetADSEcurityGroupUsers method passing in the top of hierarchy security group. This method calls the CISFLDAP object to get the top level security group’s directory entry. It then calls a method that loops through the members calling CISFLDAP for each member. If the type of the member is a user, it saves the user in a collection. If the type of the member is a security group, the method will recurse and get the members for this new security group.

    1: using System;
    2: using System.DirectoryServices;
    3: using System.Collections.Generic;
    4: using System.Text;
    5: using System.Collections.ObjectModel;
    6:  
    7: namespace MyNamespace
    8: {
    9:     public class GetAllUsers
   10:     {
   11:         /// <summary>
   12:         /// This method takes security group alias and fetches list of user alias that are member of this SG
   13:         /// </summary>
   14:         /// <param name="sgAlias">Security group alias</param>
   15:         /// <returns>List of String</returns>
   16:         public List<String> GetADSecurityGroupUsers(String sgAlias)
   17:         {
   18:             string path = "GC://DC=corp,DC=microsoft,DC=com";
   19:             string filter;
   20:             string filterFormat = "(cn={0})";
   21:             filter = String.Format(filterFormat, sgAlias);
   22:  
   23:             //Get all the AD directory entries of this security group.
   24:             PropertyCollection properties = CISFLDAP.GetADPropertiesForSingleEntry(path, filter);
   25:  
   26:             List<string> groupMembers= new List<string>();
   27:  
   28:             if (properties != null)
   29:             {
   30:                 //Used to limit AD search of members to only security groups and users.
   31:                 //The filter is built here but only really needed in the recursive method called below.
   32:                 //Was placed here for performance reasons.  Has no affect on AD call in this method.
   33:                 Collection<int> sAMAccountTypes = new Collection<int>();
   34:                 sAMAccountTypes.Add((int)AuthZGlobals.sAMAccountType.SecurityGroup);
   35:                 sAMAccountTypes.Add((int)AuthZGlobals.sAMAccountType.User);
   36:  
   37:                 //Builds the filter based on the sAMAccountTypes in the collection.
   38:                 string sAMAccountFilter = CISFLDAP.MakesAMAccountTypeQueryString(sAMAccountTypes);
   39:  
   40:                 //Look up all the members of this directory entry and look for person data
   41:                 //to add to the collection.
   42:                 groupMembers = GetUsersInGroup(properties, groupMembers, sAMAccountFilter); //GetUsersInGroup do not return anything. 
   43:             }
   44:  
   45:             return groupMembers;
   46:         }
   47:  
   48:         #region GetUsersInGroup
   49:         /// <summary>
   50:         /// Recurses through the group's member records and collects all the users.
   51:         /// </summary>
   52:         /// <param name="properties">The collection of the directory entry's properties.</param>
   53:         /// <param name="groupMembers">List of users found. </param>
   54:         /// <param name="filter">sAMAccountTypes to filter the AD search on.</param>
   55:         private List<string> GetUsersInGroup(PropertyCollection properties, List<String> groupMembers, string filter)
   56:         {
   57:             string pathFormat = "GC://{0}";
   58:             string memberIdx = "member";
   59:             string sAMAccountNameIdx = "sAMAccountName";
   60:             string sAMAccountTypeIdx = "sAMAccountType";
   61:             string personnelNumberIdx = "extensionAttribute4";
   62:  
   63:             if (properties[memberIdx] != null)
   64:             {
   65:                 foreach (object property in properties[memberIdx])
   66:                 {
   67:                     string distinguishedName = property.ToString();
   68:  
   69:                     string path = String.Format(pathFormat, distinguishedName);
   70:  
   71:                     //Get the directory entry for this member record.  Filters for only the sAMAccountTypes
   72:                     //security group and user.
   73:                     PropertyCollection childProperties = CISFLDAP.GetADPropertiesForSingleEntry(path, filter);
   74:  
   75:                     if (childProperties != null)
   76:                     {
   77:                         //If the member's sAMAccountType is User.
   78:                         if ((int)childProperties[sAMAccountTypeIdx].Value == (int)AuthZGlobals.sAMAccountType.User)
   79:                         {
   80:                             //If member is a user then add, else member is a Service Account with no 
   81:                             //personnel number.
   82:                             if (childProperties[personnelNumberIdx].Value != null) 
   83:                                 groupMembers.Add(searchResults.Properties[sAMAccountNameIdx].Value.ToString());
   84:                             else
   85:                             {
   86:                                 //You'll need to decide if you want to capture the Service Acount info.
   87:                             }
   88:                         }
   89:                         else
   90:                             //RECURSE - Look up all the members of the security group just found.
   91:                             GetUsersInGroup(childProperties, groupMembers, filter); // entry is not declared anywhere
   92:                     }
   93:                 }
   94:             }
   95:         }
   96:  
   97:         #endregion