开发者

Fast way to get a list of group members in Active Directory with C#

In a web app, we're looking to display a list of sam accounts for users that are a member of a certain group. Groups could have 500 or more members in many cases and we need the page to be responsive.

With a group of about 500 members it takes 7-8 seconds to get a list of sam accounts for all members of the group. Are there faster ways? I know the Active Directory Manag开发者_JAVA百科ement Console does it in under a second.

I've tried a few methods:

1)

PrincipalContext pcRoot = new PrincipalContext(ContextType.Domain)
GroupPrincipal grp = GroupPrincipal.FindByIdentity(pcRoot, "MyGroup");
List<string> lst = grp.Members.Select(g => g.SamAccountName).ToList();

2)

PrincipalContext pcRoot = new PrincipalContext(ContextType.Domain)
GroupPrincipal grp = GroupPrincipal.FindByIdentity(pcRoot, "MyGroup");
PrincipalSearchResult<Principal> lstMembers = grp.GetMembers(true);
List<string> lst = new List<string>();
foreach (Principal member in lstMembers )
{
    if (member.StructuralObjectClass.Equals("user"))
    {
        lst.Add(member .SamAccountName);
    }
}

3)

PrincipalContext pcRoot = new PrincipalContext(ContextType.Domain)
GroupPrincipal grp = GroupPrincipal.FindByIdentity(pcRoot, "MyGroup");
System.DirectoryServices.DirectoryEntry de = (System.DirectoryServices.DirectoryEntry)grp.GetUnderlyingObject();
List<string> lst = new List<string>();
foreach (string sDN in de.Properties["member"])
{
    System.DirectoryServices.DirectoryEntry deMember = new System.DirectoryServices.DirectoryEntry("LDAP://" + sDN);
    lst.Add(deMember.Properties["samAccountName"].Value.ToString());
}


If you want speed, don't use the System.DirectoryServices.AccountManagement namespace at all (GroupPrincipal, UserPrincipal, etc.). It makes coding easier, but it's slloooowwww.

Use only DirectorySearcher and DirectoryEntry. (The AccountManagement namespace is just a wrapper for this anyway)

I had this discussion with someone else not too long ago. You can read the full chat here, but in one case where a group had 4873 members, AccountManagement's GetMember() method took 200 seconds, where using DirectoryEntry took only 16 seconds.

However, there are a few caveats:

  1. Do not look at the memberOf attribute (as JPBlanc's answer suggests). It will not find members of Domain Local groups. The memberOf attribute only shows Universal groups, and Global groups only on the same domain. Domain Local groups don't show up there.
  2. Looking at the member attribute of a group will only feed you the members 1500 at a time. You have to retrieve the members in batches of 1500.
  3. It's possible for an account to have the primaryGroupId set to any group, and be considered part of that group (but not show up in the member attribute of that group). This is usually only the case with the Domain Users group.
  4. If a Domain Local group has users from an external domain, they show up as Foreign Security Principal objects, which hold the SID for the actual account on the external domain. Some extra work needs to be done to find the account on the external domain.

The AccountManagement namespace's GetMember() method takes care of all these things, just not as efficiently as it could.

When helping that other user, I did put together a method that will cover the first three issues above, but not #4. It's the last code block in this answer: https://stackoverflow.com/a/49241443/1202807

Update:

(I've documented all of this on my site here: Find all the members of a group)

You mentioned that the most time-consuming part is looping through the members. That's because you're binding to each member, which is understandable. You can lessen that by calling .RefreshCache() on the DirectoryEntry object to load only the properties you need. Otherwise, when you first use Properties, it'll get every attribute that has a value, which adds time for no reason.

Below is an example I used. I tested with a group that has 803 members (in nested groups) and found that having the .RefreshCache() lines consistently shaved off about 10 seconds, if not more (~60s without, ~45-50s with).

This method will not account for points 3 & 4 that I mentioned above. For example, it will silently ignore Foreign Security Principals. But if you only have one domain with no trusts, you have no need to care.

private static List<string> GetGroupMemberList(DirectoryEntry group, bool recurse = false) {
    var members = new List<string>();

    group.RefreshCache(new[] { "member" });

    while (true) {
        var memberDns = group.Properties["member"];
        foreach (var member in memberDns) {
            var memberDe = new DirectoryEntry($"LDAP://{member}");
            memberDe.RefreshCache(new[] { "objectClass", "sAMAccountName" });
            if (recurse && memberDe.Properties["objectClass"].Contains("group")) {
                members.AddRange(GetGroupMemberList(memberDe, true));
            } else {
                var username = memberDe.Properties["sAMAccountName"]?.Value?.ToString();
                if (!string.IsNullOrEmpty(username)) { //It will be null if this is a Foreign Security Principal
                    members.Add(username);
                }
            }
        }

        if (memberDns.Count == 0) break;

        try {
            group.RefreshCache(new[] {$"member;range={members.Count}-*"});
        } catch (COMException e) {
            if (e.ErrorCode == unchecked((int) 0x80072020)) { //no more results
                break;
            }
            throw;
        }
    }

    return members;
}


Here is a recursive search (search users in nested groups) using ADSI.

static void Main(string[] args)
{
  /* Connection to Active Directory
   */
  string sFromWhere = "LDAP://SRVENTR2:389/dc=societe,dc=fr";
  DirectoryEntry deBase = new DirectoryEntry(sFromWhere, "societe\\administrateur", "test.2011");

  /* To find all the users member of groups "Grp1"  :
   * Set the base to the groups container DN; for example root DN (dc=societe,dc=fr) 
   * Set the scope to subtree
   * Use the following filter :
   * (member:1.2.840.113556.1.4.1941:=CN=Grp1,OU=MonOu,DC=X)
   */
  DirectorySearcher dsLookFor = new DirectorySearcher(deBase);
  dsLookFor.Filter = "(&(memberof:1.2.840.113556.1.4.1941:=CN=Grp1,OU=MonOu,DC=societe,DC=fr)(objectCategory=user))";
  dsLookFor.SearchScope = SearchScope.Subtree;
  dsLookFor.PropertiesToLoad.Add("cn");
  dsLookFor.PropertiesToLoad.Add("samAccountName");  

  SearchResultCollection srcUsers = dsLookFor.FindAll();

  /* Just show each user
   */
  foreach (SearchResult srcUser in srcUsers)
  {
    Console.WriteLine("{0}", srcUser.Path);
    Console.WriteLine("{0}", srcUser.Properties["samAccountName"][0]);
  }

  Console.ReadLine();

}


For @Gabriel Luci comment : Microsoft documentation

memberOf

The memberOf attribute is a multi-valued attribute that contains groups of which the user is a direct member, except for the primary group, which is represented by the primaryGroupId. Group membership is dependent on the domain controller (DC) from which this attribute is retrieved:

  • At a DC for the domain that contains the user, memberOf for the user is complete with respect to membership for groups in that domain; however, memberOf does not contain the user's membership in domain local and global groups in other domains.

  • At a GC server, memberOf for the user is complete with respect to all universal group memberships. If both conditions are true for the DC, both sets of data are contained in memberOf.

Be aware that this attribute lists the groups that contain the user in their member attribute—it does not contain the recursive list of nested predecessors. For example, if user O is a member of group C and group B and group B were nested in group A, the memberOf attribute of user O would list group C and group B, but not group A.

This attribute is not stored—it is a computed back-link attribute.


A coworker of mine had similar issues with query times when using various Active Directory retrieval methods. He ended up caching the information in a database and refreshing it nightly and then just access the database instead.

Considering the fact that User Accounts don't change all that often, this was an acceptable compromise for him. Depending on your usage this may or may not be acceptable.


Similar to your first option, I created a hashset from the list. The larger the group the longer it takes to verify membership. However it is consistent for successful and unsuccessful membership queries. To iterate through a large group would sometime take 3x longer if the account wasn't a member whereas this method is the same every time.

using(PrincipalContext ctx = new PrincipalContext(ContextType.Domain))
using(GroupPrincipal group = GroupPrincipal.FindByIdentity(ctx, IdentityType.SamAccountName, "groupName"))
{
    List<string> members = group.GetMembers(true).Select(g => g.SamAccountName).ToList();
    HashSet<string> hashset = new HashSet<string>(members, StringComparer.OrdinalIgnoreCase);

    if(hashset.Contains(someUser)
        return true;
}

Caching Group Membership

Group membership in Active Directory shouldn't frequently change. For this reason, consider caching group membership to make lookups quicker. Then update the cached group membership every hour or whatever makes the most sense for your environment. This will greatly enhance performance and reduce congestion on the network and domain controllers.

One caveat is if important/restricted information is being protected and there's a need for stronger security controls. Then directly querying Active Directory is the way to go as it ensures you have the most current membership information.


Try this not sure if it would be any faster but....

        PrincipalContext pcRoot = new PrincipalContext(ContextType.Domain)

        GroupPrincipal mygroup = new GroupPrincipal(pcRoot);

        // define the principal searcher, based on that example principal

        PrincipalSearcher ps = new PrincipalSearcher(mygroup);

        ps.QueryFilter = new GroupPrincipal(pcRoot) { SamAccountName = "Name of your group Case Sensitive" };

        List<UserPrincipal> users = new List<UserPrincipal>();
        // loop over all principals found by the searcher

        GroupPrincipal foundGroup = (GroupPrincipal)ps.FindOne();

foreach (UserPrincipal u in foundGroup.Members) 
                    {
                        users.Add(u);

                    }
//OR
List<string> lst = foundGroup.Members.Select(g => g.SamAccountName).ToList();//this will only get the usernames not the user object or UserPrincipal


Have you tried an LDAP query? The bottom of the page has an example in C# for enumerating through a group to get members. MSDN BOL

using System;
using System.DirectoryServices;

namespace ADAM_Examples
{
class EnumMembers
{
    /// <summary>
    /// Enumerate AD LDS groups and group members.
    /// </summary>
    [STAThread]
    static void Main()
    {
        DirectoryEntry objADAM;                   // Binding object.
        DirectoryEntry objGroupEntry;             // Group Results.
        DirectorySearcher objSearchADAM;          // Search object.
        SearchResultCollection objSearchResults;  // Results collection.
        string strPath;                           // Binding path.

        // Construct the binding string.
        strPath = "LDAP://localhost:389/OU=TestOU,O=Fabrikam,C=US";
        Console.WriteLine("Bind to: {0}", strPath);
        Console.WriteLine("Enum:    Groups and members.");

        // Get the AD LDS object.
        try
        {
            objADAM = new DirectoryEntry(strPath);
            objADAM.RefreshCache();
        }
        catch (Exception e)
        {
            Console.WriteLine("Error:   Bind failed.");
            Console.WriteLine("         {0}", e.Message);
            return;
        }

        // Get search object, specify filter and scope,
        // perform search.
        try
        {
            objSearchADAM = new DirectorySearcher(objADAM);
            objSearchADAM.Filter = "(&(objectClass=group))";
            objSearchADAM.SearchScope = SearchScope.Subtree;
            objSearchResults = objSearchADAM.FindAll();
        }
        catch (Exception e)
        {
            Console.WriteLine("Error:   Search failed.");
            Console.WriteLine("         {0}", e.Message);
            return;
        }

        // Enumerate groups and members.
        try
        {
            if (objSearchResults.Count != 0)
            {
                foreach(SearchResult objResult in objSearchResults)
                {
                    objGroupEntry = objResult.GetDirectoryEntry();
                    Console.WriteLine("Group    {0}",
                        objGroupEntry.Name);
                    foreach(object objMember
                        in objGroupEntry.Properties["member"])
                    {
                        Console.WriteLine(" Member: {0}",
                            objMember.ToString());
                    }
                }
            }
            else
            {
                Console.WriteLine("Results: No groups found.");
            }
        }
        catch (Exception e)
        {
            Console.WriteLine("Error:   Enumerate failed.");
            Console.WriteLine("         {0}", e.Message);
            return;
        }

        Console.WriteLine("Success: Enumeration complete.");
        return;
    }
}

}

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜