J Weezy
J Weezy

Reputation: 3935

Active Directory: Get all group members

Question: How do I retrieve all group members in a consistent manner?

Context: I am retrieving all objects that are either person, group, contact, or computer:

Filter = "(|(objectCategory=person)(objectCategory=computer)(objectCategory=group))"

I now need to retrieve all members of groups. I have developed three methods to do this; however, they are returning different results for the same group and I am not sure why. I suspect that it may be caused by nested groups (i.e., groups within groups). Debugging it is a challenge because some groups contain so many members that the debugger times out and shows no results.

Method 1 and 2 is slow. Method 3 is fast. So, I prefer to use method 3.

Group Name      Retrieval Method    Recursive Search    Count Members   Comment
Group1          AccountManagement   TRUE                505 
Group1          AccountManagement   FALSE               505 
Group1          DirectoryServices   N/A                 101 
Group2          AccountManagement   TRUE                440             Contains group name 'Group3'
Group2          AccountManagement   FALSE               440             Contains group name 'Group3'
Group2          DirectoryServices   N/A                 100             Contains group name 'Group3'
Group3          AccountManagement   TRUE                101             All Group3
Group3          AccountManagement   FALSE               2               All Group3
Group3          DirectoryServices   N/A                 2               1 user 1 group (Group2)

Method 1 and 2: Get group members using the S.DS.AM, where the GetMembers() is set to either true or false, respectively: https://msdn.microsoft.com/en-us/library/system.directoryservices.accountmanagement(v=vs.110).aspx

private static List<Guid> GetGroupMemberList(string strPropertyValue, string strDomainController, bool bolRecursive)
        {
            List<Guid> listGroupMemberGuid = null;
            GroupPrincipal groupPrincipal = null;
            PrincipalSearchResult<Principal> listPrincipalSearchResult = null;
            List<Principal> listPrincipalNoNull = null;
            PrincipalContext principalContext = null;
            ContextType contextType;
            IdentityType identityType;

            try
            {
                listGroupMemberGuid = new List<Guid>();
                contextType = ContextType.Domain;
                principalContext = new PrincipalContext(contextType, strDomainController);
                identityType = IdentityType.Guid;

                groupPrincipal = GroupPrincipal.FindByIdentity(principalContext, identityType, strPropertyValue);

                if (groupPrincipal != null)
                {
                    listPrincipalSearchResult = groupPrincipal.GetMembers(bolRecursive);
                    listPrincipalNoNull = listPrincipalSearchResult.Where(item => item.Name != null).ToList();
                    foreach (Principal principal in listPrincipalNoNull)
                    {
                        listGroupMemberGuid.Add((Guid)principal.Guid);
                    }
                }
                return listGroupMemberGuid;
            }
            catch (MultipleMatchesException)
            {
                throw new MultipleMatchesException(strPropertyValue);
            }
            catch (Exception ex)
            {
                throw ex;
            }
            finally
            {
                listGroupMemberGuid = null;
                listPrincipalSearchResult.Dispose();
                principalContext.Dispose();
                groupPrincipal.Dispose();
            }
        }

Method 3: Get group members using the S.DS.AD: https://msdn.microsoft.com/en-us/library/system.directoryservices.activedirectory(v=vs.110).aspx

private static List<string> GetGroupMemberList(string strPropertyValue, string strActiveDirectoryHost, int intActiveDirectoryPageSize)
        {
            List<string> listGroupMemberDn = new List<string>();
            string strPath = strActiveDirectoryHost + "/<GUID=" + strPropertyValue + ">";
            DirectoryEntry directoryEntryGroup;
            DirectoryEntry directoryEntryGroupMembers;
            DirectorySearcher directorySearcher;
            SearchResultCollection searchResultCollection;
            DataTypeConverter objConverter = null;

            objConverter = new DataTypeConverter();

            try
            {
                directoryEntryGroup = new DirectoryEntry(strPath, null, null, AuthenticationTypes.Secure);
                directoryEntryGroup.RefreshCache();
            }
            catch (Exception ex)
            {
                throw ex;
            }

            try
            {
                directorySearcher = new DirectorySearcher(directoryEntryGroup)
                {
                    //Filter = "(objectCategory=group)", // Group
                    SearchScope = SearchScope.Subtree,
                    PageSize = intActiveDirectoryPageSize,
                };
                directorySearcher.PropertiesToLoad.Add("objectGUID");
                searchResultCollection = directorySearcher.FindAll();
            }
            catch (Exception ex)
            {
                throw ex;
            }

            try
            {
                foreach (SearchResult searchResult in searchResultCollection)
                {
                    directoryEntryGroupMembers = searchResult.GetDirectoryEntry();

                    foreach (object objGroupMember in directoryEntryGroupMembers.Properties["member"])
                    {
                        listGroupMemberDn.Add((string)objGroupMember);
                    }
                }
                return listGroupMemberDn;
            }
            catch (Exception ex)
            {
                throw ex;
            }
            finally
            {
                listGroupMemberDn = null;
                strPath = null;
                directoryEntryGroup.Dispose();
                directoryEntryGroupMembers = null;
                directorySearcher.Dispose();
                searchResultCollection.Dispose();
                objConverter = null;
            }
        }

Method 4: (Loop with GetNextChunk() method implementation)

 private static List<string> GetGroupMemberList(string strPropertyValue, string strActiveDirectoryHost, int intActiveDirectoryPageSize)
    {
        // Variable declaration(s).
        List<string> listGroupMemberDn = new List<string>();
        string strPath = strActiveDirectoryHost + "/<GUID=" + strPropertyValue + ">";
        string strMemberPropertyRange = null;
        DirectoryEntry directoryEntryGroup = null;
        DirectorySearcher directorySearcher = null;
        SearchResultCollection searchResultCollection = null;
        // https://msdn.microsoft.com/en-us/library/windows/desktop/ms676302(v=vs.85).aspx
        const int intIncrement = 1500;

        // Load the DirectoryEntry.
        try
        {
            // Setup a secure connection with Active Directory (AD) using Kerberos by setting the directoryEntry with AuthenticationTypes.Secure.
            directoryEntryGroup = new DirectoryEntry(strPath, null, null, AuthenticationTypes.Secure);

            // Load the property values for this DirectoryEntry object into the property cache.
            directoryEntryGroup.RefreshCache();
        }
        catch (Exception)
        { }

        #region Method1
        // Enumerate group members.
        try
        {
            // Check to see if the group has any members.
            if (directoryEntryGroup.Properties["member"].Count > 0)
            {
                int intStart = 0;
                while (true)
                {
                    // End of the range.
                    int intEnd = intStart + intIncrement - 1;

                    strMemberPropertyRange = string.Format("member;range={0}-{1}", intStart, intEnd);

                    directorySearcher = new DirectorySearcher(directoryEntryGroup)
                    {
                        // Set the Filter criteria that is used to constrain the search within AD.
                        Filter = "(|(objectCategory=person)(objectCategory=computer)(objectCategory=group))", // User, Contact, Group, Computer objects

                        // Set the SearchScope for how the AD tree is searched (Default = Subtree).
                        SearchScope = SearchScope.Base,

                        // The PageSize value should be set equal to the PageSize that is set by the AD administrator (Default = 0).
                        PageSize = intActiveDirectoryPageSize,

                        PropertiesToLoad = { strMemberPropertyRange }
                    };

                    try
                    {
                        // Populate the searchResultCollection with all records within AD that match the Filter criteria.
                        searchResultCollection = directorySearcher.FindAll();

                        foreach (SearchResult searchResult in searchResultCollection)
                        {
                            var membersProperties = searchResult.Properties;

                            var membersPropertyNames = membersProperties.PropertyNames.OfType<string>().Where(n => n.StartsWith("member;"));

                            foreach (var propertyName in membersPropertyNames)
                            {
                                // Get all members from the ranged result.
                                var members = membersProperties[propertyName];

                                foreach (string memberDn in members)
                                {
                                    // Add the member's "distinguishedName" attribute value to the list.
                                    listGroupMemberDn.Add(memberDn);
                                }
                            }
                        }
                    }
                    catch (DirectoryServicesCOMException)
                    {
                        // When the start of the range exceeds the number of available results, an exception is thrown and we exit the loop.
                        break;
                    }

                    // Increment for the next range.
                    intStart += intIncrement;
                }
            }

            // return the listGroupMemberDn;
            return listGroupMemberDn;
        }
        #endregion

        finally
        {
            // Cleanup objects.
            listGroupMemberDn = null;
            strPath = null;
            strMemberPropertyRange = null;
            directoryEntryGroup.Dispose();
            directorySearcher.Dispose();
            searchResultCollection.Dispose();
        }
    }

Upvotes: 1

Views: 8091

Answers (1)

Gabriel Luci
Gabriel Luci

Reputation: 40858

System.DirectoryServices.AccountManagement can be more convenient, since it hides much of the complexity of AD, but that is also what makes it slower. You have less control over what's going on.

DirectoryEntry gives you more control, but you have to handle some of the complexity.

So that can explain the time difference.

But your method that uses DirectoryEntry still seems overly complicated. Why use DirectorySearcher? It doesn't seem to add anything. You already have the group when you set directoryEntryGroup. After that, you can access the members:

foreach (var member in directoryEntryGroup.Properties["member"]) {
    //member is a string of the distinguishedName
}

For very large groups, be aware that by default AD limits the records it returns to 1500. So once you hit that number, you will have to ask for more. You do that like this:

directoryEntryGroup.RefreshCache("member;range=1500-*")

Then loop through them again the same way. If you get exactly another 1500, then ask for more (replacing the 1500 with 3000), etc. until you have them all.

This is exactly what the .NET Core implementation of System.DirectoryServices.AccountManagement does (and I presume the .NET 4.x does too - I just can't see that code). You can see the special class .NET Core code uses to do this here (see the GetNextChunk method): https://github.com/dotnet/corefx/blob/0eb5e7451028e9374b8bb03972aa945c128193e1/src/System.DirectoryServices.AccountManagement/src/System/DirectoryServices/AccountManagement/AD/RangeRetriever.cs

As a side note:

catch (Exception ex)
{
    // Something went wrong. Throw an error.
    throw ex;
}

If you are going to re-throw an exception without doing anything else, just don't catch it. Re-throwing has the effect of hiding where the exception actually happened, because your stack trace will now say that the exception happened at throw ex;, rather than tell you the actual line where the exception happened.

Even for your last block, you can use try/finally without catch.

Upvotes: 2

Related Questions