Active Directory: DirectoryEntry 成员列表 <> GroupPrincipal.GetMembers()

J W*_*ezy 1 c# ldap active-directory

我有一个小组,我们称之为 GotRocks。我正在尝试获取其所有成员,但是在 DirectoryEntry 和 AccountManagement 之间我得到了非常不同的结果。以下是会员检索方式的统计:

Method 1: DirectoryEntry.PropertyName.member = 350
Method 2: AccountManagement.GroupPrincipal.GetMembers(false) = 6500
Method 2: AccountManagement.GroupPrincipal.GetMembers(true) = 6500
Run Code Online (Sandbox Code Playgroud)

作为健全性检查,我进入 ADUC 并从组中提取成员列表,默认情况下限制为 2,000。这里重要的是 ADUC 似乎验证了 AccountManagement 结果。我也检查了 Children 属性,但它是空白的。此外,DirectoryEntry 中列出的成员都不属于 SchemaName 组 - 他们都是用户。

我不认为这是代码问题,但可能缺乏对 DirectoryEntry 和 GetMembers 方法如何检索组成员的理解。谁能解释为什么 DirectoryEntry 成员列表会产生与 GetMembers 递归函数不同的结果?是否有我需要注意的某种方法或属性?注意:我构建了一个函数,该函数将通过“member;range={0}-{1}”查询 DirectoryEntry,其中循环以 1,500 的块为单位获取成员。我在这里完全不知所措。

DirectoryEntry 返回的结果如此之少这一事实是有问题的,因为我想使用 DirectoryEntry 作为一个简单的事实,即走这条路线至少比 AccountManagement 快两个数量级(即,秒表时间为 1,100 毫秒与 250,000 毫秒) .

更新 1:方法:

方法 1:目录条目

private 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
    {
        directoryEntryGroup = new DirectoryEntry(strPath, null, null, AuthenticationTypes.Secure);

        directoryEntryGroup.RefreshCache();
    }
    catch (Exception)
    { }

    try
    {
        if (directoryEntryGroup.Properties["member"].Count > 0)
        {
            int intStart = 0;

            while (true)
            {
                int intEnd = intStart + intIncrement - 1;

                // Define the PropertiesToLoad attribute, which contains a range flag that LDAP uses to get a list of members in a pre-specified chunk/block of members that is defined by each loop iteration.
                strMemberPropertyRange = string.Format("member;range={0}-{1}", intStart, intEnd);

                directorySearcher = new DirectorySearcher(directoryEntryGroup)
                {
                    Filter = "(|(objectCategory=person)(objectCategory=computer)(objectCategory=group))", // User, Contact, Group, Computer objects

                    SearchScope = SearchScope.Base,

                    PageSize = intActiveDirectoryPageSize,

                    PropertiesToLoad = { strMemberPropertyRange }
                };

                try
                {
                    searchResultCollection = directorySearcher.FindAll();

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

                        // Find the property that starts with the PropertyName of "member;" and get all of its member values.
                        var membersPropertyNames = membersProperties.PropertyNames.OfType<string>().Where(n => n.StartsWith("member;"));

                        // For each record in the memberPropertyNames, get the PropertyName and add to the lest.
                        foreach (var propertyName in membersPropertyNames)
                        {
                            var members = membersProperties[propertyName];

                            foreach (string memberDn in members)
                            {                                   
                                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;
                }

                intStart += intIncrement;
            }
        }

        return listGroupMemberDn;
    }
    finally
    {
        listGroupMemberDn = null;
        strPath = null;
        strMemberPropertyRange = null;
        directoryEntryGroup.Close();
        if(directoryEntryGroup != null) directoryEntryGroup.Dispose();                
        if (directorySearcher != null) directorySearcher.Dispose();
        if(searchResultCollection != null) searchResultCollection.Dispose();
    }
}
Run Code Online (Sandbox Code Playgroud)

方法 2:AccountManagement(将 bolRecursive 切换为 true 或 false)。

private List<Guid> GetGroupMemberList(string strPropertyValue, string strDomainController, bool bolRecursive)
{
    // Variable declaration(s).
    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);

        // Setup the IdentityType. Use IdentityType.Guid because GUID is unique and never changes for a given object. Make sure that is what strPropertyValue is receiving.
        // This is required, otherwise you will get a MultipleMatchesException error that says "Multiple principals contain a matching Identity."
        // This happens when you have two objects that AD thinks match whatever you're passing to UserPrincipal.FindByIdentity(principalContextDomain, strPropertyValue)
        identityType = IdentityType.Guid;

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

        if (groupPrincipal != null)
        {
            // Get all members that the group contains and add it to the list.
            // Note: The true flag in GetMembers() specifies a recursive search, which enables the application to search a group recursively and return only principal objects that are leaf nodes.
            listPrincipalSearchResult = groupPrincipal.GetMembers(bolRecursive);

            // Remove the nulls from the list, otherwise the foreach loop breaks prematurly on the first null found and misses all other object members.
            listPrincipalNoNull = listPrincipalSearchResult.Where(item => item.Name != null).ToList();

            foreach (Principal principal in listPrincipalNoNull)
            {
                listGroupMemberGuid.Add((Guid)principal.Guid);
            }
        }

        return listGroupMemberGuid;
    }
    catch (MultipleMatchesException)
    {
        // Multiple principals contain a matching identity. 
        // In other words, the same property value was found on more than one record in either of the six attributes that are listed within the IdentityType enum.
        throw new MultipleMatchesException(strPropertyValue);
    }
    finally
    {
        // Cleanup objects.
        listGroupMemberGuid = null;
        if(listPrincipalSearchResult != null) listPrincipalSearchResult.Dispose();
        if(principalContext != null) principalContext.Dispose();
        if(groupPrincipal != null) groupPrincipal.Dispose();
    }
}
Run Code Online (Sandbox Code Playgroud)

更新 2:

public static void Main()
{
    Program objProgram = new Program();

    // Other stuff here.

    objProgram.GetAllUserSingleDc();

    // Other stuff here.
}

private void GetAllUserSingleDc()
{
    string strDomainController = "domain.com"; 
    string strActiveDirectoryHost = "LDAP://" + strDomainController;
    int intActiveDirectoryPageSize = 1000;
    string[] strAryRequiredProperties = null;
    DirectoryEntry directoryEntry = null;
    DirectorySearcher directorySearcher = null;
    SearchResultCollection searchResultCollection = null;
    DataTypeConverter objConverter = null;
    Type fieldsType = null;

    fieldsType = typeof(AdUserInfoClass);

    objConverter = new DataTypeConverter();

    directoryEntry = new DirectoryEntry(strActiveDirectoryHost, null, null, AuthenticationTypes.Secure);

    directorySearcher = new DirectorySearcher(directoryEntry)
    {
        //Filter = "(|(objectCategory=person)(objectCategory=computer)(objectCategory=group))", // User, Contact, Group, Computer objects
        Filter = "(sAMAccountName=GotRocks)", // Group

        SearchScope = SearchScope.Subtree,

        PageSize = intActiveDirectoryPageSize

        PropertiesToLoad = { "isDeleted","isCriticalSystemObject","objectGUID","objectSid","objectCategory","sAMAccountName","sAMAccountType","cn","employeeId",
                            "canonicalName","distinguishedName","userPrincipalName","displayName","givenName","sn","mail","telephoneNumber","title","department",
                            "description","physicalDeliveryOfficeName","manager","userAccountControl","accountExpires","lastLogon","logonCount","lockoutTime",
                            "primaryGroupID","pwdLastSet","uSNCreated","uSNChanged","whenCreated","whenChanged","badPasswordTime","badPwdCount","homeDirectory",
                            "dNSHostName" }
    };         

    searchResultCollection = directorySearcher.FindAll();            

    try
    {
        foreach (SearchResult searchResult in searchResultCollection)
        {
            clsAdUserInfo.GidObjectGuid = objConverter.ConvertByteAryToGuid(searchResult, "objectGUID");            
            clsAdUserInfo.StrDirectoryEntryPath = strActiveDirectoryHost + "/<GUID=" + clsAdUserInfo.GidObjectGuid + ">";
            clsAdUserInfo.StrSchemaClassName = new DirectoryEntry(clsAdUserInfo.StrDirectoryEntryPath, null, null, AuthenticationTypes.Secure).SchemaClassName;

            if (clsAdUserInfo.StrSchemaClassName == "group")
            {
                // Calling the functions here.
                List<string> listGroupMemberDnMethod1 = GetGroupMemberListStackOverflow(clsAdUserInfo.GidObjectGuid.ToString(), strActiveDirectoryHost, intActiveDirectoryPageSize);

                List<Guid> listGroupMemberGuidMethod2 = GetGroupMemberList(clsAdUserInfo.GidObjectGuid.ToString(), strDomainController, false)
            }   
            // More stuff here.
        }
    }
    finally
    {
        // Cleanup objects.
        // Class constructors.
        objProgram = null;
        clsAdUserInfo = null;
        // Variables.
        intActiveDirectoryPageSize = -1;
        strActiveDirectoryHost = null;
        strDomainController = null;
        strAryRequiredProperties = null;
        directoryEntry.Close();
        if(directoryEntry !=null) directoryEntry.Dispose();
        if(directorySearcher != null) directorySearcher.Dispose();
        if(searchResultCollection != null) searchResultCollection.Dispose();
        objConverter = null;
        fieldsType = null;
    }   
}
Run Code Online (Sandbox Code Playgroud)

更新 3:

下面是 I am 的命名空间列表using

using System;
using System.Collections.Generic;
using System.DirectoryServices;
using System.DirectoryServices.AccountManagement;
using System.Security.Principal;
using System.Text;
using System.Linq;
using System.Collections;
Run Code Online (Sandbox Code Playgroud)

更新 4:Program.cs

using System;
using System.Collections.Generic;
using System.DirectoryServices;
using System.DirectoryServices.AccountManagement;
using System.Security.Principal;
using System.Text;
using System.Linq;

namespace activeDirectoryLdapExamples
{
    public class Program
    {
         public static void Main()
        {
            Program objProgram = new Program();
            objProgram.GetAllUserSingleDc();
        }

        #region GetAllUserSingleDc
        private void GetAllUserSingleDc()
        {
            Program objProgram = new Program();
            string strDomainController = "EnterYourDomainhere";
            string strActiveDirectoryHost = "LDAP://" + strDomainController;
            int intActiveDirectoryPageSize = 1000;
            DirectoryEntry directoryEntry = null;
            DirectorySearcher directorySearcher = null;
            SearchResultCollection searchResultCollection = null;
            DataTypeConverter objConverter = null;

            objConverter = new DataTypeConverter();

            directoryEntry = new DirectoryEntry(strActiveDirectoryHost, null, null, AuthenticationTypes.Secure);

            directorySearcher = new DirectorySearcher(directoryEntry)
            {
                Filter = "(sAMAccountName=GotRocks)", // Group

                SearchScope = SearchScope.Subtree,

                PageSize = intActiveDirectoryPageSize,

                PropertiesToLoad = { "isDeleted","isCriticalSystemObject","objectGUID","objectSid","objectCategory","sAMAccountName","sAMAccountType","cn","employeeId",
                                        "canonicalName","distinguishedName","userPrincipalName","displayName","givenName","sn","mail","telephoneNumber","title","department",
                                        "description","physicalDeliveryOfficeName","manager","userAccountControl","accountExpires","lastLogon","logonCount","lockoutTime",
                                        "primaryGroupID","pwdLastSet","uSNCreated","uSNChanged","whenCreated","whenChanged","badPasswordTime","badPwdCount","homeDirectory",
                                        "dNSHostName" }
            };

            searchResultCollection = directorySearcher.FindAll();

            try
            {
                foreach (SearchResult searchResult in searchResultCollection)
                {                    
                    Guid? gidObjectGuid = objConverter.ConvertByteAryToGuid(searchResult, "objectGUID");
                    string StrSamAccountName = objConverter.ConvertToString(searchResult, "sAMAccountName");
                    // Get new DirectoryEntry and retrieve the SchemaClassName from it by binding the current objectGUID to it.
                    string StrDirectoryEntryPath = strActiveDirectoryHost + "/<GUID=" + gidObjectGuid + ">";
                    string StrSchemaClassName = new DirectoryEntry(StrDirectoryEntryPath, null, null, AuthenticationTypes.Secure).SchemaClassName;

                    #region GetGroupMembers
                    if (StrSchemaClassName == "group")
                    {
                        // FAST!
                        var watch = System.Diagnostics.Stopwatch.StartNew();
                        List<string> listGroupMemberDn = GetGroupMemberList(gidObjectGuid.ToString(), strActiveDirectoryHost, intActiveDirectoryPageSize);
                        watch.Stop();
                        var listGroupMemberDnElapsedMs = watch.ElapsedMilliseconds;

                        // SLOW!
                        watch = System.Diagnostics.Stopwatch.StartNew();
                        List<Guid> listGroupMemberGuidRecursiveTrue = GetGroupMemberList(gidObjectGuid.ToString(), strDomainController, true);
                        watch.Stop();
                        var listGroupMemberGuidRecursiveTrueElapsedMs = watch.ElapsedMilliseconds;

                        watch = System.Diagnostics.Stopwatch.StartNew();
                        List<Guid> listGroupMemberGuidRecursiveFalse = GetGroupMemberList(gidObjectGuid.ToString(), strDomainController, false);
                        watch.Stop();
                        var listGroupMemberGuidRecursiveFalseElapsedMs = watch.ElapsedMilliseconds;

                        ////// Display all members of the list.
                        //listGroupMemberDn.ForEach(item => Console.WriteLine("Member GUID: {0}", item));
                        //listGroupMemberGuidRecursiveTrue.ForEach(item => Console.WriteLine("Member GUID: {0}", item));
                        //listGroupMemberGuidRecursiveFalse.ForEach(item => Console.WriteLine("Member GUID: {0}", item));

                        Console.WriteLine("objectGUID: {0}", gidObjectGuid);
                        Console.WriteLine("sAMAccountName: {0}", strSamAccountName);

                        // Result: 350
                        Console.WriteLine("\nlistGroupMemberDn Count Members: {0}", listGroupMemberDn.Count);
                        Console.WriteLine("Total RunTime listGroupMemberDnElapsedMs (in milliseconds): {0}", listGroupMemberDnElapsedMs);

                        // Result: 6500
                        Console.WriteLine("\nlistGroupMemberGuidRecursiveTrue Count Members: {0}", listGroupMemberGuidRecursiveTrue.Count);
                        Console.WriteLine("Total RunTime listGroupMemberGuidRecursiveTrueElapsedMs (in milliseconds): {0}", listGroupMemberGuidRecursiveTrueElapsedMs);

                        // Result: 6500
                        Console.WriteLine("\nlistGroupMemberGuidRecursiveFalse Count Members: {0}", listGroupMemberGuidRecursiveFalse.Count);
                        Console.WriteLine("Total RunTime listGroupMemberGuidRecursiveFalseElapsedMs (in milliseconds): {0}", listGroupMemberGuidRecursiveFalseElapsedMs);
                        Console.WriteLine("\n");
                    }
                    #endregion

                    #region CurrentSearchResult
                    else
                    {
                        Console.WriteLine("ObjectGuid = {0}", gidObjectGuid);
                        Console.WriteLine("SamAccountName = {0}", strSamAccountName);

                    }
                    #endregion
                }

                Console.WriteLine("\nPress any key to continue.");
                Console.ReadKey();
            }
            finally
            {
                objProgram = null;
                intActiveDirectoryPageSize = -1;
                strActiveDirectoryHost = null;
                strDomainController = null;
                directoryEntry.Close();
                if (directoryEntry != null) directoryEntry.Dispose();
                if (directorySearcher != null) directorySearcher.Dispose();
                if (searchResultCollection != null) searchResultCollection.Dispose();
                objConverter = null;
            }
        }
        #endregion

        #region GetGroupMemberListGuid
        private List<Guid> GetGroupMemberList(string strPropertyValue, string strDomainController, bool bolRecursive)
        {
            List<Guid> listGroupMemberGuid = null;
            List<Principal> listPrincipalNoNull = null;
            GroupPrincipal groupPrincipal = null;
            PrincipalSearchResult<Principal> listPrincipalSearchResult = 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 li

Gab*_*uci 5

最后一个代码块(更新 2)就是答案!

您用于读取member属性的代码比它需要的更复杂。它返回偏斜结果可能是有原因的,但我看起来并不太难,因为您根本不需要使用DirectorySearcher。我刚刚重写了它。

这是它应该是什么样子,它是最简单的形式:

private static List<string> GetGroupMemberList(string groupGuid, string domainDns) {
    var members = new List<string>();

    var group = new DirectoryEntry($"LDAP://{domainDns}/<GUID={groupGuid}>", null, null, AuthenticationTypes.Secure);

    while (true) {
        var memberDns = group.Properties["member"];
        foreach (var member in memberDns) {
            members.Add(member.ToString());
        }

        if (memberDns.Count == 0) break;

        try {
            group.RefreshCache(new[] {$"member;range={members.Count}-*", "member"});
        } catch (System.Runtime.InteropServices.COMException e) {
            if (e.ErrorCode == unchecked((int) 0x80072020)) { //no more results
                break;
            }
            throw;
        }
    }
    return members;
}
Run Code Online (Sandbox Code Playgroud)

像这样调用它:

var members = GetGroupMemberList("00000000-0000-0000-0000-000000000000", "domain.com");
Run Code Online (Sandbox Code Playgroud)

这不是递归的。为了使其递归,您必须DirectoryEntry从每个成员创建一个新成员并测试它是否是一个组,然后获取该组的成员。

我打开了代码,所以这是递归版本。它很慢,因为它必须绑定到每个成员以查看它是否是一个组。

这不是防弹的。在某些情况下,您可能会得到奇怪的结果(例如,如果您的用户位于组中受信任的外部域中)。

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

    var group = new DirectoryEntry($"LDAP://{domainDns}/<GUID={groupGuid}>", null, null, AuthenticationTypes.Secure);

    while (true) {
        var memberDns = group.Properties["member"];
        foreach (var member in memberDns) {
            if (recurse) {
                var memberDe = new DirectoryEntry($"LDAP://{member}");
                if (memberDe.Properties["objectClass"].Contains("group")) {
                    members.AddRange(GetGroupMemberList(
                        new Guid((byte[]) memberDe.Properties["objectGuid"].Value).ToString(), domainDns,
                        true));
                } else {
                    members.Add(member.ToString());
                }
            } else {
                members.Add(member.ToString());
            }
        }

        if (memberDns.Count == 0) break;

        try {
            group.RefreshCache(new[] {$"member;range={members.Count}-*", "member"});
        } catch (System.Runtime.InteropServices.COMException e) {
            if (e.ErrorCode == unchecked((int) 0x80072020)) { //no more results
                break;
            }
            throw;
        }
    }
    return members;
}
Run Code Online (Sandbox Code Playgroud)

更新: 我确实必须编辑您的GetMembers示例,因为它不断向我抛出异常。我注释掉了该.Where行并更改foreach了将成员添加到列表的循环:

        //listPrincipalNoNull = listPrincipalSearchResult.Where(item => item.Name != null).ToList();
        if (groupPrincipal != null) {
            foreach (Principal principal in listPrincipalSearchResult) {
                listGroupMemberGuid.Add(((DirectoryEntry)principal.GetUnderlyingObject()).Guid);
            }
        }
Run Code Online (Sandbox Code Playgroud)

当然,这是编译一个 Guid 列表而不是 DN。

更新 2:这是一个版本,它也拉取以该组为主要组(但未列在member该组的属性中)的用户。GetMembers似乎这样做。将用户创建的组作为主要组会很奇怪,但在技术上是可能的。部分内容复制自此处的答案:如何检索组中的用户,包括主要组用户

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

    var members = new List<string>();

    // The count result returns 350.
    var group = new DirectoryEntry(strPath, null, null, AuthenticationTypes.Secure);
    //var group = new DirectoryEntry($"LDAP://{"EnterYourDomainHere"}/<GUID={strPropertyValue}>", null, null, AuthenticationTypes.Secure);

    while (true)
    {
        var memberDns = group.Properties["member"];
        foreach (var member in memberDns)
        {
            members.Add(member.ToString());
        }

        if (memberDns.Count < intIncrement) break;

        group.RefreshCache(new[] { $"member;range={members.Count}-*" });
    }

    //Find users that have this group as a primary group
    var secId = new SecurityIdentifier(group.Properties["objectSid"][0] as byte[], 0);

    /* Find The RID (sure exists a best method)
     */
    var reg = new Regex(@"^S.*-(\d+)$");
    var match = reg.Match(secId.Value);
    var rid = match.Groups[1].Value;

    /* Directory Search for users that has a particular primary group
     */
    var dsLookForUsers =
        new DirectorySearcher {
            Filter = string.Format("(primaryGroupID={0})", rid),
            SearchScope = SearchScope.Subtree,
            PageSize = 1000,
            SearchRoot = new DirectoryEntry(strActiveDirectoryHost)
    };
    dsLookForUsers.PropertiesToLoad.Add("distinguishedName");

    var srcUsers = dsLookForUsers.FindAll();

    foreach (SearchResult user in srcUsers)
    {
        members.Add(user.Properties["distinguishedName"][0].ToString());
    }
    return members;
}
Run Code Online (Sandbox Code Playgroud)