The finer points of .NET DirectoryServices

The finer points of .NET DirectoryServices

In the past few days I’ve been tasked with writing a .NET service, part of which must do the following:

  • Read all the user accounts from Active Directory.
  • Identify which accounts are disabled.
  • Find out what groups each user belongs to.

Achieving these three simple goals with .NET’s DirectoryServices proved to be surprisingly difficult. Here are workarounds for three common issues you might encounter.

First challenge: Getting more than 1000 results

The first problem I encountered was my DirectorySearcher object only returning 1000 results from FindAll(). I wanted all 6000. This limit was being imposed due to some funny behaviour with DirectorySearcher’s SizeLimit property, which defaults to 1000. If you try to set it higher, it will max out at the server’s limit, which (you guessed it) also defaults to 1000.

The trick is to set the DirectorySearcher’s PageSize value to less than 1000. All the results will be silently paged back from the server in the background, ignoring the SizeLimit.

using (DirectorySearcher searcher = new DirectorySearcher(this.searchRoot)){        // Search for user objects...        searcher.Filter = "(&(objectClass=user)(objectCategory=person))";        // Set the PageSize between 0 and the max page size (1000) to return all        // results at once (invisibly paged on demand in the background). Otherwise,        // a limit of 1000 results is imposed.        searcher.PageSize = 500;        using (SearchResultCollection results = searcher.FindAll())        {                foreach (SearchResult result in results)                {                        DirectoryEntry directoryEntry = result.GetDirectoryEntry();                        // ... process user                }        }}

Second challenge: Identifying disabled accounts

DirectorySearcher and DirectoryEntry have a collection of properties that provide useful information from Active Directory like the sAMAccountName, memberOf list, location and e-mail address.

Unfortunately, no property exists to identify if the account has been disabled. To get around this, we have to use a bitwise OR on the UserAccountControl flags to see if ACCOUNTDISABLE is set.

// Check and see if the ACCOUNTDISABLE flag is set.const int ACCOUNTDISABLE = 0x0002;int flags = (int)directoryEntry.Properties["userAccountControl"].Value;bool isDisabled = Convert.ToBoolean(flags & ACCOUNTDISABLE);

Third challenge: Finding all of a user’s groups

DirectoryEntry has a property called memberOf that, at first glance, looks like an easy-to-use list of all the user’s groups. Unfortunately, under Windows 2000, this collection excludes the user’s primary group.

To get an unabridged list of groups, I used a different method that assembles the account’s tokenGroups (a list of SIDs) into an OR-query, and enumerates the results. Here’s what it looked like:

directoryEntry.RefreshCache(new string[]{"tokenGroups"});// Start building a new LDAP OR query.StringBuilder sb = new StringBuilder();sb.Append("(|");// Attach each tokenGroup's SID to the query.foreach (byte[] sid in directoryEntry.Properties["tokenGroups"])        sb.AppendFormat("(objectSid={0})", BuildOctetString(sid));sb.Append(")");StringCollection groups = new StringCollection();using (DirectorySearcher searcher = new DirectorySearcher(this.searchRoot)){        // Apply a filter from our query, and load the name property of each        // object found.        searcher.Filter = sb.ToString();        searcher.PropertiesToLoad.Add("name");        using (SearchResultCollection results = searcher.FindAll())        {                // Get each group's name, and add it to our StringCollection.                foreach (SearchResult result in results)                        groups.Add(result.Properties["name"][0].ToString());        }}...// Helper function to convert a binary SID into a string format suitable for use// in an LDAP query.static string BuildOctetString(byte[] bytes){        StringBuilder sb = new StringBuilder();                                foreach (byte b in bytes)                sb.AppendFormat("\{0}", b.ToString("X2"));        return sb.ToString();}

Note that with tokenGroups, you might get more groups than expected. It contains all nested security groups, not just the user’s immediate groups.