无法成功通过AccountManagement.GroupPrincipal GetMembers对象进行迭代

时间:2018-11-30 22:28:40

标签: c# active-directory

我在C#中使用System.DirectoryServices.AccountManagement.GroupPrincipal FindByIdentity创建一个对象,其中包含目标组的组成员(用户ID和名称)。我的目标是遍历结果的UserPrincipals列表,并为每个列表打印SamAccountName和DisplayName。对于某些目标群体来说,这很好。对于其他用户,它在导致以下错误的用户(或多个用户)上失败:

  

System.DirectoryServices.AccountManagement.PrincipalOperationException   HResult = 0x80131501   Message =指定的目录服务属性或值不存在。

当我使用PowerShell的Get-ADGroup获取失败目标之一的组对象并对其进行遍历时,没有问题。

我研究了AD组的成员身份,我相信问题在于,在某些组(失败的组)中,某些成员可能已被禁用,或者可能是跨域信任的一部分。但是,他们的身份对我来说无关紧要。我只想列出所有内容,以便组所有者可以决定将哪些成员迁移到新组。

我使用的方法是:

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;
}

谁能建议我如何遍历UserPrincipals列表而不抛出PrincipalOperationException?还是至少有一种方法可以绕过导致这些错误的UserPrincipal出现?即使我无法列出失败的用户,我也将幸免于难。

1 个答案:

答案 0 :(得分:0)

不幸的是,正如您所发现的,System.DirectoryServices.AccountManagement名称空间不能与国外安全委托人一起很好地发挥作用。

您可以使用System.DirectoryServices在后​​台使用的AccountManagement名称空间来实现。尽管它有点复杂,但您可能仍会发现它的性能更好。

无论如何我一直想为my website编写这样的内容,因此这里提供了一种方法,该方法将查找组中的所有成员并以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属性,因此不会找到将该组作为主要组的用户。我在我的What makes a member a member文章中对此进行了描述。在大多数情况下,您都不会在意。