Thursday, December 22, 2005

Resurrecting Tombstones in Active Directory or ADAM

I did not get to put all the material I wanted to into the book, so I figure some of the stuff we have left out will make good material for a blog entry here and there.

Active Directory and ADAM both have a special container called “CN=Deleted Objects” that can only be viewed by administrators by default.  When we delete objects in either of these directories, the entries are typically moved there and most of their attributes are stripped off.  This is done such that other replication partners know which items were deleted.  These objects are now referred to as ‘tombstones’.  The system will hold these objects for a set period of time (60 days or so IIRC) before purging them completely.

Now, occasionally we run into situations where we want to resurrect one of the tombstones.  This is typically when we need an object to have the identical GUID or SID it had while alive as simply recreating the object would generate new values for these attributes.  There is an example on MSDN on how to resurrect these objects, but it is in C++.  I thought it would be fun to show how to do this in System.DirectoryServices.Protocols as it has the capabilities to do this while System.DirectoryServices (ADSI) does not.  We have to use the LDAP replace method in this case which is supported in the new Protocols namespace.

Instead of just posting a link to my code, I thought I would post it in whole here.  Notice that my Lazarus (resurrected, get it?) class is using Well-known GUID binding.  You should have your RootDSE return a defaultNamingContext for ADAM if you have not already done so.

class Lazarus : IDisposable
{
    const string WK_TOMBSTONE = "18e2ea80684f11d2b9aa00c04f79f805";
    string _defaultNC;
    string _server;
 
    DirectoryEntry _entry;
 
    public Lazarus()
        : this(null)
    {
    }
 
    public Lazarus(string server)
    {
        _server = server;
 
        DirectoryEntry root = new DirectoryEntry(
            String.Format(
                "LDAP://{0}RootDSE",
                _server != null ? _server + "/" : String.Empty
                )
            );
 
        using (root)
        {
            _defaultNC = root.Properties["defaultNamingContext"][0].ToString();
        }
 
        _entry = new DirectoryEntry(
            String.Format(
                "LDAP://{0}<WKGUID={1},{2}>",
                _server != null ? _server + "/" : String.Empty,
                WK_TOMBSTONE,
                _defaultNC
                ),
            null,
            null,
            AuthenticationTypes.Secure
            | AuthenticationTypes.FastBind
            );
    }
 
    public void ShowAllTombStones()
    {
 
        DirectorySearcher ds = new DirectorySearcher(
            _entry,
            "(isDeleted=TRUE)",
            null,
            System.DirectoryServices.SearchScope.OneLevel
            );
 
        ds.PageSize = 1000;
        ds.Tombstone = true;
 
        using (SearchResultCollection src = ds.FindAll())
        {
            foreach (SearchResult sr in src)
            {
                foreach (string key in sr.Properties.PropertyNames)
                {
                    foreach (object o in sr.Properties[key])
                    {
                        Console.WriteLine("{0}: {1}", key, o);
                    }
                }
                Console.WriteLine("====================");
            }
        }
 
    }
 
    private SearchResult GetTombstone(string name)
    {
        DirectorySearcher ds = new DirectorySearcher(
            _entry,
            String.Format("(&(isDeleted=TRUE)(name={0}*))", name),
            new string[] { "cn", "lastKnownParent", "distinguishedName" },
            System.DirectoryServices.SearchScope.OneLevel
            );
 
        ds.Tombstone = true;
 
        return ds.FindOne();
    }
 
    public void RaiseFromTheDead(string name)
    {
        SearchResult deadGuy = GetTombstone(name);
 
        if (deadGuy == null)
            throw new ArgumentException("No object exists");
 
        LdapConnection conn = new LdapConnection(
            new LdapDirectoryIdentifier(_server),
            System.Net.CredentialCache.DefaultNetworkCredentials,
            AuthType.Negotiate
            );
 
        using (conn)
        {
            conn.Bind();
            conn.SessionOptions.ProtocolVersion = 3;
 
            //we have to remove the isDELETED attribute
            DirectoryAttributeModification dam = new DirectoryAttributeModification();
            dam.Name = "isDELETED";
            dam.Operation = DirectoryAttributeOperation.Delete;
 
            //and set a new dn string - a bit of a copout because
            //I am always assuming a CN prefix.
            string newDN = String.Format(
                "CN={0},{1}",
                deadGuy.Properties["cn"][0].ToString().Split(new char[]{'\n'})[0],
                deadGuy.Properties["lastKnownParent"][0]
                );
 
            //word of warning... 'lastKnownParent' is only guaranteed good
            //on Windows Server 2003 and ADAM.
 
            DirectoryAttributeModification dam2 = new DirectoryAttributeModification();
            dam2.Name = "distinguishedName";
            dam2.Operation = DirectoryAttributeOperation.Replace;
            dam2.Add(newDN);
 
            //kinda bizzare that a collection is not used
            ModifyRequest mr = new ModifyRequest(
                deadGuy.Properties["distinguishedName"][0].ToString(),
                new DirectoryAttributeModification[] { dam, dam2 }
                );
 
            //we need to have the ShowDeletedControl to find this DN
            mr.Controls.Add(new ShowDeletedControl());
 
            //optionally, we must put any required attributes on the object
            //as well.  For user/computer, this might be 'sAMAccountName'
 
            ModifyResponse resp = (ModifyResponse)conn.SendRequest(mr);
 
            if (resp.ResultCode != ResultCode.Success)
            {
                //we should also check to see if it was already
                //existing here (same DN).
                Console.WriteLine(resp.ErrorMessage);
            }
        }
    }
 
    #region IDisposable Members
 
    public void Dispose()
    {
        if (_entry != null)
            _entry.Dispose();
    }
 
    #endregion
}

This is definitely sample code, but it should give a working idea of how to accomplish this.  I think that as I used the .Protocols namespace, there were some usability issues that I question, however it is definitely a neat addition.  I decided to mix using System.DirectoryServices with using .Protocols because I wanted to show how to do a Tombstone search using SDS as well.

Finally, this was tested on ADAM, but should work fine on AD.  The usual warnings apply:  This is not production code, don’t expect it to be and don’t cry to me if it breaks your machine horribly or your dog dies.

Enjoy, your comments are always welcome.