Skip to content

Caching AD info using DirectorySynchronization

17/07/2012

A client had a bespoke identity management app which cached AD group information so they could have autocomplete of group names on a self-service access request webpage. To keep their cache updated they were querying each DC in the domain (over 30) to get the highestCommittedUSN, then picking one at random to get groups with a newer USN. In this way they would never have to do a full sync, even if the DC they usually talked to failed (crazy way of doing it!). We needed a better solution, and I knew a way to do this using standard MS tools.

The standard System.DirectoryServices.DirectorySearcher can be configured to take a DirectorySynchronization control which saves a cookie of the directory state when it is run, so only deltas are shown when you run next time. The directory searcher can contain your usual LDAP string, in my case (objectClass=group).

I simply created a standard search using DirectorySearcher and then added a new DirectorySynchronization control. On first run, this has the effect of taking a baseline (all groups in this case) and dumping them into a list (this took say 20 secs for 70k groups). A cookie is generated which stores information about the state of the directory at this instant, and I get this with GetDirectorySynchronizationCookie() then write out to a text file (approx 1KB in size).

On subsequent runs, this cookie file is read using ResetDirectorySynchronizationCookie() and used to tell the searcher to only pickup changes since last time it was run then output these as a list. Depending on which attributes you specify to return you can get all sorts of changes here, but I specifically wanted add/delete/rename of groups. By specifying ExtendedDN and name as the to attributes this gave me name, DN, objectGUID and objectSID which was sufficient for the identity management app to update its cache.

The changes seen are add, delete and rename. Delete is obvious (see below). Whether you add or rename a group it appears the same but of course having the SID or GUID your app can see that this is either a new object or rename of an existing object.

This even works when the baseline is generated on one DC and the delta on another. Full code is below:

// a class to contain a single group result
    public class GroupInfo
    {
        public string Dn { get; set; }
        public string Name { get; set; }
        public string ObjectGuid { get; set; }
        public string ObjectSid { get; set; }
    }

    public static List<GroupInfo> GetGroups()
    {
        try
        {
            string sLdapServer = "LDAP://yourdc";
            string sLdapFilter = "(objectClass=group)";
            
            // file where cookie will be stored
            string sCookiePath = @"c:\temp\dirsync.dat";
            
            // configure directory search
            DirectoryEntry dir = new DirectoryEntry(sLdapServer);
            DirectorySearcher searcher = new DirectorySearcher(dir);

            searcher.Filter = sLdapFilter;
            searcher.PropertiesToLoad.Add("name");
            searcher.PropertiesToLoad.Add("distinguishedName");
            searcher.SearchScope = SearchScope.Subtree;
            searcher.ExtendedDN = ExtendedDN.Standard;

            // create new dirsync object
            DirectorySynchronization sync = new DirectorySynchronization();

            // check whether a cookie file exists and if so, set the dirsync to use it
            if (File.Exists(sCookiePath))
            {
                byte[] byteCookie = File.ReadAllBytes(sCookiePath);
                sync.ResetDirectorySynchronizationCookie(byteCookie);
            }

            // assign dirsync object to the searcher object
            searcher.DirectorySynchronization = sync;

            // iterate over search results and enter into a list
            List<GroupInfo> liGroups = new List<GroupInfo>();

            foreach (SearchResult result in searcher.FindAll())
            {
                GroupInfo giGroup = new GroupInfo();


                giGroup.Name = (string)result.Properties["name"][0];
                string[] sExtendedDn = ((string)result.Properties["distinguishedName"][0]).Split(new Char[] {';'});
                giGroup.Dn = sExtendedDn[2];
                giGroup.ObjectGuid = sExtendedDn[0].Substring(6, 36);
                giGroup.ObjectSid = sExtendedDn[1].Substring(5, sExtendedDn[1].Length - 6);
                liGroups.Add(giGroup);
            }

            // write new cookie value to file
            File.WriteAllBytes(sCookiePath,sync.GetDirectorySynchronizationCookie());

            return liGroups;

        }

        // catch LDAP errors
        catch (Exception exc)
        {
            List<GroupInfo> liGroups = new List<GroupInfo>();
            GroupInfo giGroup = new GroupInfo();
            giGroup.Name = String.Format("Error attempting to get data from LDAP: {0} ", exc.Message);
            liGroups.Add(giGroup);
            return liGroups;
        }
    }
Advertisements

From → Active Directory, C#

2 Comments
  1. While this code “just works” there are caveats. The caveats are numerous. They include: you can get dups, taking a cookie across DCs can be gnarly, and you need to code defensive for a full sync (ie cookie not working from time to time). Things to keep in mind…

  2. Thanks for the comments Eric. I know my limits with programming and this wasn’t mission critical so I figured it was robust enough for that. What is actually stored in that cookie?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: