首页 > 解决方案 > Cannot successfully iterate through AccountManagement.GroupPrincipal GetMembers Object

问题描述

I am using System.DirectoryServices.AccountManagement.GroupPrincipal FindByIdentity in C# to create an object containing the group members (user IDs and names) for the target group. My goal is to iterate through the resulting list of UserPrincipals and print the SamAccountName and DisplayName for each. For some target groups, this is working fine; for others it fails on a user (or perhaps more than one) that throws the following error:

System.DirectoryServices.AccountManagement.PrincipalOperationException HResult=0x80131501 Message=The specified directory service attribute or value does not exist.

When I use PowerShell’s Get-ADGroup to get the group object for one of the failing targets and iterate through it, there is no problem.

I’ve looked into the AD Group memberships and I believe the problem is that in some groups (those failing), some members may have been disabled, or may be part of a cross-domain trust. However, their status is of no consequence to me; I just want to list everything so the group owner can decide which members get migrated to new groups.

The method I am using is:

private static ArrayList EnumerateGroupMembers()
{
    ArrayList gmObjects = new ArrayList();
    string ldapVal = "DC=dc1,DC=dc2,DC=dcMain,DC=dcSecondary";
    string ldapDom = "dc1.dc2.dcMain.dcSecondary:389";

    PrincipalContext ctx = new PrincipalContext(ContextType.Domain, ldapDom, ldapVal);

    GroupPrincipal group = GroupPrincipal.FindByIdentity(ctx, "AD-GROUPNAME");

    if (group != null)
    {
        var users = group.GetMembers(true);

        //*** PrincipalOperationException occurs here ***
        foreach (UserPrincipal p in users)
        {
            Console.WriteLine(p.SamAccountName + ", " + p.DisplayName);
        }
        Console.WriteLine("Done");
        Console.ReadKey();
    }
    //*** Please note: I know I am returning an empty list here. I'm writing to Console during development
    return gmObjects;
}

Can anyone suggest how I can iterate through the list of UserPrincipals without throwing a PrincipalOperationException? Or, at least a way to bypass the UserPrincipal occurrences that are causing these errors? Even if I cannot list the failing users I will survive.

标签: c#active-directory

解决方案


不幸的是System.DirectoryServices.AccountManagement,正如您所发现的,命名空间与外国安全主体不能很好地配合使用。

您可以使用System.DirectoryServices名称空间来执行此操作,这是AccountManagement在幕后使用的。无论如何,您可能会发现它的性能更好,尽管它有点复杂。

无论如何,我一直想为我的网站写一些类似的东西,所以这里有一种方法可以找到一个组的所有成员并以DOMAIN\username格式列出它们。它也可以选择扩展嵌套组。

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

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

    if (domainSidMapping == null) {
        //Find all the trusted domains and create a dictionary that maps the domain's SID to its DNS name
        var groupCn = (string) group.Properties["canonicalName"].Value;
        var domainDns = groupCn.Substring(0, groupCn.IndexOf("/", StringComparison.Ordinal));

        var domain = Domain.GetDomain(new DirectoryContext(DirectoryContextType.Domain, domainDns));
        var trusts = domain.GetAllTrustRelationships();

        domainSidMapping = new Dictionary<string, string>();

        foreach (TrustRelationshipInformation trust in trusts) {
            using (var trustedDomain = new DirectoryEntry($"LDAP://{trust.TargetName}")) {
                try {
                    trustedDomain.RefreshCache(new [] {"objectSid"});
                    var domainSid = new SecurityIdentifier((byte[]) trustedDomain.Properties["objectSid"].Value, 0).ToString();
                    domainSidMapping.Add(domainSid, trust.TargetName);
                } catch (Exception e) {
                    //This can happen if you're running this with credentials
                    //that aren't trusted on the other domain or if the domain
                   //can't be contacted
                   Console.WriteLine($"Can't connect to domain {trust.TargetName}: {e.Message}");
                }
            }
        }
    }

    while (true) {
        var memberDns = group.Properties["member"];
        foreach (string member in memberDns) {
            using (var memberDe = new DirectoryEntry($"LDAP://{member.Replace("/", "\\/")}")) {
                memberDe.RefreshCache(new[] { "objectClass", "msDS-PrincipalName", "cn" });

                if (recurse && memberDe.Properties["objectClass"].Contains("group")) {
                    members.AddRange(GetGroupMemberList(memberDe, true, domainSidMapping));
                } else if (memberDe.Properties["objectClass"].Contains("foreignSecurityPrincipal")) {
                    //User is on a trusted domain
                    var foreignUserSid = memberDe.Properties["cn"].Value.ToString();
                    //The SID of the domain is the SID of the user minus the last block of numbers
                    var foreignDomainSid = foreignUserSid.Substring(0, foreignUserSid.LastIndexOf("-"));
                    if (domainSidMapping.TryGetValue(foreignDomainSid, out var foreignDomainDns)) {
                        using (var foreignUser = new DirectoryEntry($"LDAP://{foreignDomainDns}/<SID={foreignUserSid}>")) {
                            foreignUser.RefreshCache(new[] { "msDS-PrincipalName" });
                            members.Add(foreignUser.Properties["msDS-PrincipalName"].Value.ToString());
                        }
                    } else {
                        //unknown domain
                        members.Add(foreignUserSid);
                    }
                } else {
                    var username = memberDe.Properties["msDS-PrincipalName"].Value.ToString();
                    if (!string.IsNullOrEmpty(username)) {
                        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;
}

这有几件事要做:

  1. member属性一次只会为您提供 1500 个帐户,因此您必须要求更多,直到没有剩余为止。
  2. Foreign Security Principal 拥有外域帐户的 SID,但您需要使用域的 DNS 名称连接到它(即$"LDAP://{foreignDomainDns}/<SID={foreignUserSid}>")。所以这个方法将查找域的所有信任,并在域的 SID 和它的 DNS 名称之间创建一个映射。

你像这样使用它:

var group = new DirectoryEntry($"LDAP://{distinguishedNameOfGroup}");
var members = GetGroupMemberList(group);

或者,GetGroupMemberList(group, true)如果您也想在嵌套组中查找用户,也可以使用。

请记住,这不会找到将此组作为主要组的用户,因为主要组不使用该member属性。我在我的什么让会员成为会员一文中对此进行了描述。在大多数情况下,您不会在意。


推荐阅读