Saturday, August 13, 2005

AD, LDAP and the urge to merge

Lately I've been working on code (don't call it a script - this is real code) to "migrate" user profiles to a new domain as part of a merger. Both companies have large Active Directory infrastructures, but one had to be chosen as the "post merger domain". However, it isn't physically possible to re-image everyone's machine the same day and just magically have them all logging on to a new domain as soon as regulatory and stockholder approvals are done. During that interim state that comes after the approvals, but before everyone is in one happy domain - there exists a need for certain tools.

Let's say that a user from company A is has a machine in domain He logs on to that same domain. However, after the merger he wants to logon to company B's domain - Put up the requisite trusts, migrate the groups and accounts with SID History and he can certainly do that, right? But - what happens to his "My Documents" folder, his application configurations and special toolbars, and most importantly of all the pictures of his kids that he uses as a screensaver? Logon to a brand new domain - you get a brand new NT profile, right? All that other stuff is MIA.

Enter profile migration. As I'm sure everyone knows, Windows NT based systems like Windows 2000 and Windows XP store pointers to your profile on disk. Browse to HKLM\Software\Microsoft\Windows NT\CurrentVersion\ProfileList and you'll see what I mean. Each SID shown there is the SID of a user (or system account). Under each is a "ProfileImagePath" that shows where the NTUser.dat (current user registry hive) and application settings, "my documents" etc. live. So, assuming the correct SID History migrations were done, all you need to do is get the NEW SID into the registry here and copy the info from the OLD SID over, reboot the machine and have them login with their new account. Presto! Their ugly kids are still on their desktops! The trick, when working with non-Admin accounts is to actually GET the new sid in there and copy the data over.

First, you need to locate the NEW account. Realize that in a large merger it is very likely that you will hit at least some name collisions between the two domains, so you can't just assume that user A\username should now logon as B\username. You have to find them by sid history since their new account may be named something different. I first tried "LookupAccountSID" since it was a Windows API call that was kinda short (unlike the horrible "ConvertStringSecurityDescriptorToSecurityDescriptorW" - how's that for a verbosity monstrosity, huh?). LookupAccountSid seemed good since it is documented as being able to lookup by primary SID or SID history. However, in practice - since you will need to have a trust up - it finds the old SID back in the old domain, even when you take pains to force the lookup to happen on a DC in the new domain. So, scratch that one.

Next, I thought I'd try LDAP instead. Turns out that LDAP is really only good at searching string data. So, when you have a blob type object like a SID, you have to mangle into a strange looking string first that looks something like this: \00\05\01\EA\... To do this, pass the SID pointer to ADsEncodeBinaryData which will convert it to a "funny string" for use in an LDAP query filter. Now, you can try something like "(ldap://dc=b,dc=company,dc=com");(&(objectClass=User)(sidHistory=" & sSID & "));name,samAccountName,ADsPath;subtree" to lookup that pesky primary SID from domain in domain Except - you can't!

Nope, you can't make that query. Remember, the computer is in domain, the user is logged on with their old account in, and if you try that LDAP query against the domain that trusts yours you get a "table does not exist" error. Thanks AD!! Thanks LDAP!! Nope, you end up having to first call DsGetDcName to find a domain controller in the other domain and send the LDAP query directly to it like this: "(ldap://;(&(objectClass=User)(sidHistory=" & sSID & "));name,samAccountName,ADsPath;subtree". Now, you've got the ADsPath of the user, you can use GetObject to get that user object, grab off the primary SID of the user, convert it to a string and... Whoops, you don't have Admin rights so you can't just pop that into the registry...

Now, what you have to do is ask the user for their password in the new domain and call CreateProcessWithLogonW to run your code again as the NEW user - passing in the old SID as a string on the command line. (Hopefully your user didn't run your code from a mapped drive: the reason for this is left as an excercise for the reader). Make sure to use the proper parameters to cause a user profile to be created. This will create those entries needed under ProfileList in the registry. Now that they exist, you just update them. Meanwhile, the first copy of your program is waiting for the second new one to exit. Since you have the old SID from the command line, you simply replicate the ProfileList values from the old to the new in the registry. Then the second copy can exit. The first copy might not have rights to create the new entries, but it DOES have read access. So the first copy reads the entries to make sure that they are correct. Now it can notify the user of success, inform them that they must logon with their new account and reboot the machine.

The user logs on with their new account, sees their ugly kids on their desktop and all is right with their world. They never knew the work that their friendly neighborhood geek had to go through to make this stuff work. They probably think the feature was built into Windows.

No comments: