Wednesday 5 May 2010

How to Recursively Get the Group Membership of a User in Active Directory using .NET/C# and LDAP (without just 2 hits to Active Directory)

Problem
What should you do if you need to find all the indirect group memberships that a user has in Active Directory? While it is possible to recursively navigate (ie traverse) through all the group structures that your user is a member of, this can be a very intensive process and can potentially involve 100s of calls to the LDAP server (A slight performance hit to say the list. In sum, BAD!)

In the example below, how do we determine that a user is a member of a top level group without making intensive, recursive calls to Active Directory/LDAP?


Solution
A better option is to use the power of [Microsoft's Implementation] of LDAP to get the results in only 2 hits to the server.
  1. We start of with the user's login name (e.g. david.klein)
  2. We query ldap to get their Container Name (CN) e.g. CN=David Klein
  3. We use the special query syntax provided by Microsoft LDAP in the Directory Searcher Filter to recursively get a list of all groups that the user is directly AND indirectly a member of.
Details

See source code below for 2 helper methods you can use to recursively determine if the designated user is directly or indirectly a member of a particular group. Note that we use the special filter syntax using a specific member flag that will get all indirect memberships automatically for us:

"(member:1.2.840.113556.1.4.1941:=CN=My User Name,OU=Users,OU=NSW,OU=DDKONLINE,DC=DDKONLINE,DC=int)"

/// 
        /// Recursively Gets ALL nested group memberships of a user and checks the input group is there.
        /// 
        /// 
e.g. david.klein or kled123/// 
Container Name of Group e.g. "SP_DEV_HR"/// Uses following config entries
        /// 
        /// 
        /// 
        public static bool IsUserMemberOfGroup(string username, string groupname)
        {
            ///ConfigHelper.LDAPRoot is "LDAP://DC=DDKONLINE,DC=int"
            DirectoryEntry entry = new DirectoryEntry(ConfigHelper.LDAPRoot);
            // Create a DirectorySearcher object.
            DirectorySearcher mySearcher = new DirectorySearcher(entry);
            //Filter by special recursive LDAP string e.g. 
            //"(member:1.2.840.113556.1.4.1941:=CN={0},OU=Users,OU=NSW,OU=DDKONLINE,DC=DDKONLINE,DC=int)"
            mySearcher.Filter = string.Format(ConfigHelper.LDAPGroupMemberFilterRecursive, 
                GetUserContainerName(username));
            mySearcher.SearchScope = SearchScope.Subtree; //Search from base down to ALL children. 
            SearchResultCollection result = mySearcher.FindAll();
            //StringBuilder sb = new StringBuilder();

            for (int i = 0; i < result.Count - 1; i++)
            {
                if (result[i].Path.ToUpper().Contains(string.Format("CN={0}", groupname.ToUpper())))
                    return true; //Success - group found
            }
            //No match found
            return false;
        }

        /// 
        /// Gets the Container Name (CN) of the input user.
        /// 
        /// 
/// 
        public static string GetUserContainerName(string userName)
        {
            DirectoryEntry entry = new DirectoryEntry(ConfigHelper.LDAPRoot);
            // Create a DirectorySearcher object.
            DirectorySearcher mySearcher = new DirectorySearcher(entry);
            mySearcher.Filter = string.Format("(&(sAMAccountName={0}))", userName);
            mySearcher.SearchScope = SearchScope.Subtree; //Search from base down to ALL children. 
            SearchResultCollection result = mySearcher.FindAll();
            if (result.Count == 0)
                throw new ApplicationException(string.Format("User '{0}' Not Found in Active Directory.", userName));
            return result[0].GetDirectoryEntry().Name.Replace("CN=",string.Empty);  
        }

Example Unit Test Methods
/// 
        /// This Test checks that the recursive search works correctly against Active directory.
        /// ie. that it picks up indirect membership
        /// Uses following config entries
        /// 
        /// 
        /// 
        [TestMethod()]
        public void IsUserMemberOfGroup_DirectMembership_Positive_Test()
        {
            string username = "sp_dev_pdmtest1"; 
            string groupname = "SP_DEV_HR"; // TODO: Initialize to an appropriate value
            bool expected = true; // TODO: Initialize to an appropriate value
            bool actual;
            actual = ADHelper.IsUserMemberOfGroup(username, groupname);
            Assert.AreEqual(expected, actual);
        }

        /// 
        /// This Test checks that the recursive search works correctly against Active directory.
        /// ie. that it picks up indirect membership
        /// 
        [TestMethod()]
        public void IsUserMemberOfGroup_IndirectMembership_Positive_Test()
        {
            string username = "sp_dev_pdmtest1";
            //Naming Convention for Groups is Environment_AppDomain_FunctionalArea_ObjectType (e.g. Form)_Role
            string groupname = "SP_DEV_Onlineforms_Peoplemgmt_Termination_F_Contributors"; // TODO: Initialize to an appropriate value
            bool expected = true; // TODO: Initialize to an appropriate value
            bool actual;
            actual = ADHelper.IsUserMemberOfGroup(username, groupname);
                Assert.AreEqual(expected, actual);
        }

        ///This Test Checks that the container name is resolved. Container name is used by the recursive group search.
        ///
        [TestMethod()]
        public void GetUserContainerNameTest()
        {
            string username = "david.klein"; 
            string expected = "David Klein"; 
            string actual = ADHelper.GetUserContainerName(username);
            Assert.AreEqual(expected, actual);
        }


DDK

5 comments:

~PakKaramu~ said...

Pak Karamu visiting your blog

MarcJ said...

Hi David,

Thanks for your blog post on this - identifying nested groups is indeed important for audits and group management, so I'm certain your post will be helpful to many IT admins out there.

By the way, do you happen to offer any free AD Reporting Tools that can do so as well? If so, I would be interested in learning more, as I run a blog on Free Active Directory Reporting Tools, and I would be happy to consider reviewing any tools you could suggest.

In any event, even if there are no tools thusfar, I'm sure your code and tips above will certainly be helpful to folks - nice work man!

Thanks,
- Marc

Anonymous said...

David,

There are a lot of solutions posted on the net, but yours is the most elegant solution and that fixed my problem too.

So thank you.
Manabu

ubadan said...

Hi! Thank you for the solution,
it seems in enumeration you should change
for (int i = 0; i < result.Count - 1; i++)
to
for (int i = 0; i < result.Count; i++)
otherwise, the last group will not be enumerated

Anto said...

Thanks this is the right solution for nested group search in efficient way.

Great post and help thanks a lot :)