Cannot successfully iterate through AccountManagement.GroupPrincipal GetMembers Object

1

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
asked on Stack Overflow Nov 30, 2018 by Normally

1 Answer

1

Unfortunately, the System.DirectoryServices.AccountManagement namespace does not play nicely with Foreign Security Principals, as you've found.

You can do it using the System.DirectoryServices namespace, which is what AccountManagement uses behind the scenes. You will probably find it performs better anyway, although it is a little bit more complicated.

I've been meaning to write up something like this for my website anyway, so here is a method that will find all members of a group and list them in DOMAIN\username format. It has the option to expand nested groups too.

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

There are a few things this has to do:

  1. The member attribute will only give you 1500 accounts at a time, so you have to ask for more until there are none left.
  2. Foreign Security Principal's have the SID of the account on the foreign domain, but you need to use the domain's DNS name to connect to it (i.e. $"LDAP://{foreignDomainDns}/<SID={foreignUserSid}>"). So this method will look up all the trusts for the domain and create a mapping between the domain's SID and its DNS name.

You use it like this:

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

Or you can use GetGroupMemberList(group, true) if you want to find users in nested groups too.

Keep in mind that this will not find users who have this group as their primary group, since the primary group does not use the member attribute. I describe that in my What makes a member a member article. In most cases you won't care though.

answered on Stack Overflow Dec 5, 2018 by Gabriel Luci • edited Dec 12, 2018 by Gabriel Luci

User contributions licensed under CC BY-SA 3.0