It is a fact of life that there is no one source of truth for all data in most enterprises of any size. As such, we often need to build applications that integrate various sources of data through synchronization processes.
In both Active Directory and ADAM, we can track changes to the directory using an LDAP extension called the DirSync control. This extension allows a user to return the difference between the initial state of the directory and some later state. To do this, an initial search is performed that records the current state into a cookie. On a subsequent search, this cookie is handed back and is used to determine which objects have changed, and an updated cookie is returned. Since an object change could include the actual deletion of an object, this type of search automatically includes tombstones.
Warning: Some DirSync Options Require Windows 2003 Clients!
As of this writing, some parts of the ADSI support for DirSync require a Windows Server 2003 client. Please refer to the sidebar DirectorySynchronizationOptions, later in this chapter, for additional details.
Limitations on Search Root and Scope
We must use the root of one of our directory or application partitions as the SearchRoot (application, domain, configuration, or schema). The only scope allowed for this type of search is Subtree. Typically, we would use paging for a large Subtree-scoped search. However, in this type of search, paging is not allowed.
Permissions
To use this type of search, the caller must have the "directory get changes" privilege (SE_SYNC_AGENT_NAME) for the current root directory partition being searched. By default, this means only an administrator or SYSTEM will be able to perform this search. Finally, the caller must have the DS-Replication-Get-Changes control access right (which is a special type of entry applied to the security descriptor on the partition root object). As we can see, this prevents all but the most privileged users from using this functionality. These privileges are needed specifically because in essence, we are allowing the running account to view any changed attribute on every object, regardless of the underlying access control list (ACL) permission.
For Windows 2003, we have the option of using DirectorySynchronizationOption.ObjectSecurity, which allows the current user to see the changes on objects that she would already have permission to see based on her current security context. Objects that the current security credentials would not normally be allowed to see will not be returned. This is a good way to give this ability to less-privileged accounts and still allow this type of search.
By default, we should be aware that no ADAM security principal will have the DS-Replication-Get-Changes control access right by default. This will have to be added to a group or user using a tool such as ADAM's version of dsacls.exe.
Filter
Any valid filter is usable; however, we have to keep in mind that only the objects that match that filter will be tracked on the initial search and on subsequent searches. For instance, if we specified (sn=Dunn) for the initial search, only objects with a last name equal to "Dunn" would be tracked for subsequent searches. Instead, if we initially specified (objectClass=user), all users would be tracked (as well as computers). A subsequent search to find changes could specify (sn=Dunn), which would filter the results equivalently to using (&(objectClass=user)(sn=Dunn)).
Attributes
We can specify specific attributes we are interested in tracking by adding them to the PropertiesToLoad collection. If we pass null (Nothing in Visual Basic .NET), this is an indication that we wish to track all attributes. Any specified attributes in the PropertiesToLoad collection will also act as an additional filter, as only those objects on the initial search that have at least one of these attributes populated will be tracked. Subsequent searches will include only those objects where at least one of the tracked attributes has been updated. When inspecting the returned SearchResult, only the attributes that have changed will be included, regardless of whether the attribute is populated. In addition to any changed attributes, ADsPath, objectGuid, and instanceType will always be included in a SearchResult.
DirSync Samples
Listing 5.5 demonstrates a complete DirSync implementation.
Listing 5.5. Sample DirSync Class
public class DirSync { //snip... just the juicy parts left for brevity public void InitializeCookie(string qry) { //this is our searchroot DirectoryEntry entry = new DirectoryEntry( this.adsPath, this.username, this.password, AuthenticationTypes.Secure ); using (entry) { //we want to track all attributes (use null) string[] attribs = null; DirectorySearcher ds = new DirectorySearcher( entry, qry, attribs ); //we must use Subtree scope ds.SearchScope = SearchScope.Subtree; //pass in the flags we wish here DirectorySynchronization dSynch = new DirectorySynchronization( DirectorySynchronizationOptions.None ); ds.DirectorySynchronization = dSynch; using (SearchResultCollection src=ds.FindAll()) { Console.WriteLine( "Initially Found {0} objects", src.Count ); //get and store the cookie StoreCookie( dSynch.GetDirectorySynchronizationCookie() ); } } } public void GetSynchedChanges(string qry, bool saveState) { //this is our searchroot DirectoryEntry entry = new DirectoryEntry( this.adsPath, this.username, this.password, AuthenticationTypes.Secure ); using (entry) { string[] attribs = null; DirectorySearcher ds = new DirectorySearcher( entry, qry, attribs ); //we must use Subtree scope ds.SearchScope = SearchScope.Subtree; //pass back in our saved cookie DirectorySynchronization dSynch = new DirectorySynchronization( DirectorySynchronizationOptions.None, RestoreCookie() ); ds.DirectorySynchronization = dSynch; using (SearchResultCollection src=ds.FindAll()) { Console.WriteLine( "Subsequently Changed: {0} objects", src.Count ); //return each object that has changed //and what attributes have changed, //keeping in mind that the attributes: //'objectGuid', 'instanceType', and //'ADsPath' will always be returned as well foreach (SearchResult sr in src) { Console.WriteLine( "Detected Change in {0}", sr.Properties["AdsPath"][0] ); Console.WriteLine("Changed Values:"); Console.WriteLine("==============:"); foreach (string prop in sr.Properties.PropertyNames) { Console.WriteLine( " {0} : {1}", prop, sr.Properties[prop][0] ); } } if (saveState) { //get and store the cookie again StoreCookie( dSynch.GetDirectorySynchronizationCookie() ); } } } } //quick method to save the byte[] array to disk private void StoreCookie(byte[] cookieBytes) { //snipped for brevity! } //quick method to restore the byte[] array from disk private byte[] RestoreCookie() { //snipped for brevity! } } |
The class shown in Listing 5.5 has been abbreviated to cut down on the size of this book, but we provide a complete listing on the book's web site.
Our sample class can be consumed using a technique similar to that shown in Listing 5.6.
Listing 5.6. Demonstrating DirSync Class Use
//optionally add credentials if not running //with privileges DirSync ds = new DirSync(); //initialize our search and look for all users ds.InitializeCookie( "(objectClass=user)" ); //..update the directory somehow //now retrieve what has changed //this can be called much later //or in another program as state //is stored to the local drive ds.GetSynchedChanges( "(objectClass=user)", true ); |
It is important to note that we are essentially comparing two snapshots in time. Actually, that is not entirely trueit is more as though we are viewing a partial snapshot full of changes since a given point in time. We actually do not know the initial values of any object directly. We know only when something has changed, and we know its final value. We should also note that object state can differ between domain controllers, even at the same moment in time. As such, it is advisable to bind to the exact same domain controller each time when comparing snapshots. This eliminates the natural discrepancy that can occur between object states based on localized but nonreplicated changes.
One last point that bears mentioning is that we really have no way of tracking any of the intermediate changes that might have occurred between an object's initial state and its final state right before the search. An object's state can vary drastically between its initial and final states, but these intermediate values will not be captured by this type of search. Only the final state of the object will be directly viewable in these cases.
As we can see, the DirectorySynchronization class is extremely powerful if we can live with some of the limitations imposed on us.
Using Attribute Scope Query |
Part I: Fundamentals
Introduction to LDAP and Active Directory
Introduction to .NET Directory Services Programming
Binding and CRUD Operations with DirectoryEntry
Searching with the DirectorySearcher
Advanced LDAP Searches
Reading and Writing LDAP Attributes
Active Directory and ADAM Schema
Security in Directory Services Programming
Introduction to the ActiveDirectory Namespace
Part II: Practical Applications
User Management
Group Management
Authentication
Part III: Appendixes
Appendix A. Three Approaches to COM Interop with ADSI
Appendix B. LDAP Tools for Programmers
Appendix C. Troubleshooting and Help
Index