Search code examples
c#active-directoryactive-directory-groupactivedirectorymembership

Check if AD Group is Member of another group (recursive)


Imagine i have structure

RootGroup <- Group{x} .... <- Group{x+n} <- Group100 

How could I check that Group100 is member of RootGroup

I have this and it returns always false

private bool IsMemberOfInternal(string userOrGroupDistinguishedName, string groupMembershipDistinguishedName)
        {
            GroupPrincipal principal = null;
            GroupPrincipal target = null;
            try
            {
                principal = _getUserGroupPrincipalFunc(principalContext, userOrGroupDistinguishedName);
                target = _getUserGroupPrincipalFunc(principalContext, groupMembershipDistinguishedName);

                if (principal != default(GroupPrincipal)
                    && target != default(GroupPrincipal))
                {
                    return principal.IsMemberOf(target);
                }
            }
            catch
            {
            }

            return false;
        }

Solution

  • You're better off not using GroupPrincipal for this. AD actually has a built-in way to do this kind of search that is far faster than any thing GroupPrincipal can do. You can use that by using DirectoryEntry and DirectorySearcher directly (that's what GroupPrincipal and PrincipalSearcher use behind the scenes anyway).

    I wrote an article about figuring out if a user is a member of a specific group, but it applies just the same to groups. I have a sample method there that you can use for this:

    private static bool IsUserInGroup(DirectoryEntry user, DirectoryEntry group, bool recursive) {
    
        //fetch the attributes we're going to need
        user.RefreshCache(new [] {"distinguishedName", "objectSid"});
        group.RefreshCache(new [] {"distinguishedName", "groupType"});
    
        //This magic number tells AD to look for the user recursively through any nested groups
        var recursiveFilter = recursive ? ":1.2.840.113556.1.4.1941:" : "";
    
        var userDn = (string) user.Properties["distinguishedName"].Value;
        var groupDn = (string) group.Properties["distinguishedName"].Value;
    
        var filter = $"(member{recursiveFilter}={userDn})";
    
        if (((int) group.Properties["groupType"].Value & 8) == 0) {
            var groupDomainDn = groupDn.Substring(
                groupDn.IndexOf(",DC=", StringComparison.Ordinal));
            var userDomainDn = userDn.Substring(
                userDn.IndexOf(",DC=", StringComparison.Ordinal));
            if (groupDomainDn != userDomainDn) {
                //It's a Domain Local group, and the user and group are on
                //different domains, so the account might show up as a Foreign
                //Security Principal. So construct a list of SID's that could
                //appear in the group for this user
                var fspFilters = new StringBuilder();
    
                var userSid =
                    new SecurityIdentifier((byte[]) user.Properties["objectSid"].Value, 0);
                fspFilters.Append(
                    $"(member{recursiveFilter}=CN={userSid},CN=ForeignSecurityPrincipals{groupDomainDn})");
    
                if (recursive) {
                    //Any of the groups the user is in could show up as an FSP,
                    //so we need to check for them all
                    user.RefreshCache(new [] {"tokenGroupsGlobalAndUniversal"});
                    var tokenGroups = user.Properties["tokenGroupsGlobalAndUniversal"];
                    foreach (byte[] token in tokenGroups) {
                        var groupSid = new SecurityIdentifier(token, 0);
                        fspFilters.Append(
                            $"(member{recursiveFilter}=CN={groupSid},CN=ForeignSecurityPrincipals{groupDomainDn})");
                    }
                }
                filter = $"(|{filter}{fspFilters})";
            }
        }
    
        var searcher = new DirectorySearcher {
            Filter = filter,
            SearchRoot = group,
            PageSize = 1, //we're only looking for one object
            SearchScope = SearchScope.Base
        };
    
        searcher.PropertiesToLoad.Add("cn"); //just so it doesn't load every property
    
        return searcher.FindOne() != null;
    }
    

    This method also handles the case where the user (or your child group) is on an external trusted domain from the root group. That may or may not be a thing you have to worry about.

    Just pass a DirectoryEntry for your Group100 as the user parameter. Something like this:

    var isMemberOf = IsUserInGroup(
        new DirectoryEntry($"LDAP://{userOrGroupDistinguishedName}"),
        new DirectoryEntry($"LDAP://{groupMembershipDistinguishedName}"),
        true);
    

    For recursive searches (when you pass true for the recursive parameter), it uses the LDAP_MATCHING_RULE_IN_CHAIN "matching rule OID" (as explained here):

    This rule is limited to filters that apply to the DN. This is a special "extended" match operator that walks the chain of ancestry in objects all the way to the root until it finds a match.