Recently, I was doing some user migration work in a SharePoint 2010 environment and I ran into something pretty interesting. The important details of this work are as follows:
- Two separate domains exist, one is production and one is test
- Both domains share the same name (ie: contoso.com)
- Same users exist in both domains (ie: user ‘contoso\spmonkey’ exists in both the production domain and the test domain)
- Production data was duplicated from the production SharePoint to the test SharePoint environment by attaching backups of the production content databases to equivalent Web Applications in the test environment.
- Appropriate content database ownership/rights have been granted, sites in the content database are discoverable.
So far, so good. Had this been all one domain, two separate farms, users would be able to begin testing content and validating the data migration was successful. Here’s where this migration gains a little in complexity.
- An entry is typically populated into the UserInfo table of the content database in two scenarios
- If a user is directly granted permission to a SharePoint site, either via SharePoint group or permission level directly
- If a user is granted access to a SharePoint site through an Active Directory group AND browses to the SharePoint site
- You could actually populate user information into the UserInfo table using the object model. Todd Carter has some posts about this subject, including a tool.
- This is a good thing, it allows us to have certain profile information available within the content database in which the site resides – like user display names, which we use all over the place, and the user’s SID, which we can use while we calculate the rights a user has to a certain object.
- The SID for each user is different in the test domain than it is in the production domain. The production SID is stored in the UserInfo table
- Because of this, SharePoint is unable to calculate rights properly for the user logging into the site – so it seems. SharePoint is actually calculating rights just fine. An account with the same SID as the authenticated user account has never been granted access to any resources
- This is ok. We have several tools for this (stsadm –o migrateuser, or the Move-SPUser PowerShell Cmdlet, for example)
Even at this point, we’re still ok. We’re all SharePoint professionals who have dealt with domain Migrations before, or an account that has been deleted and recreated – so this is nothing new. So now we have two viable options to get out of this mess.
- We can manually use Move-SPUser or stsadm –o migrateuser on each user account
- This works well if you have 10 users, it doesn’t work so well when you have 10,000 users
- We can leverage PowerShell to enumerate all users in a particular scope (web application, site collection, etc.) and use a ForEach loop to perform a Move-SPUser Cmdlet against all user objects.
- We can write a custom .NET application to retrieve all user objects in a particular scope and use the object model to populate the user’s new SID.
- You could violate your EULA and render your environment unsupportable by writing a custom .NET application that replaces the user SID in the userinfo table with the value from the current domain. DO NOT DO THIS EVER. It seems simple, it seems to be the least resource intensive option, but it’s a HORRIBLE idea.
With our 3 viable options outlined above, (#4 not being viable), I chose to use the 2nd option and wrote a PowerShell script to do the work for me. This worked great until the Move-SPUser Cmdlet was used against the Object Cache accounts. Everybody remembers those accounts right? In case that doesn’t ring a bell, maybe “Portal Super User” and “Portal Super Reader” accounts will sound more familiar. As soon as we got past executing the Move-SPUser Cmdlet against these two accounts, even valid migrated accounts which worked earlier in the migration process received the SharePoint access denied page. Even the site collection administrator account was denied access. Even accounts granted full control via web application policies was denied access. For anybody who has seen this issue before, this is immediately identifiable as a web application which is missing entries for the Object Cache accounts. Using PowerShell, we can verify that the Portal Super User and the Portal Super Reader properties for the web application do exist and have the correct account configured. What was peculiar is that previous policies for web application had been removed as a result of running the PowerShell script.
Further testing indicates that whenever you run Move-SPUser against a user account, any web application policies defined for that account are removed from all web applications. The exception in this case appears to be for the “Search Crawling Account”. Possibly any policy where the user display name does not match the display name for the user in Active Directory – but that requires further testing. What happened in my case is that once the Move-SPUser Cmdlet was executed against the Object Cache accounts, the policies for the web applications granting full read and full control to these accounts was removed. Further to this, the full read policy granted to NT Authority\Local Service was also removed.
The fix for this issue was actually pretty simple. As part of the script, I initially record the Object Cache accounts to a variable. At the end of the script, once all user accounts have been migrated using the Move-SPUser Cmdlet, I set the Object Cache accounts to their former values (this step is optional, because they don’t actually get erased) and configure web application policies to grant the appropriate permission levels to both of these accounts, as well as the NT Authority\Local Service account. I will post the set of scripts I used for this once they are sanitized (probably a day or two).
The lesson from this blog is:
Be careful when you use the Move-SPUser Cmdlet. It can and will erase any web application policies for any user accounts against which this Cmdlet is run.
Now that we all know about this, it’s pretty simple to plan for this and to work around this issue.
Go ahead and test this out for yourself – I’d be very happy to hear if this does not reproduce in your environment, as it’s easily reproducible in 100% of the environments in which I’ve tested.