Table of Contents
Sharepoint User Domain Migration using Stsadm
Summary: Troubleshooting an Active Directory Domain Migration for Sharepoint.
Date: Around 2014
Refactor: 16 April 2025: Checked links and formatting.
While doing an AD migration we ran into a very annoying error while trying to migrate the sharepoint users. If you are a little bit familiar with Sharepoint you would know that all users are known by a unique id in the sharepoint database, and also by their domain. Now if a user changes from his or her domain, he or she would lose access to their sharepoint data. Even if you'll use the SID history option. That is of course not what you'd want so there is a migration option to change all ownerships from one use to another, stsadm -o migrateuser. However, while testing this command we ran into some errors and it took us months to find out what went wrong. It turned out users were connected to orphaned sites, so we had to clean up these orphaned sites before we could migrate these users. In this article I'll show you everything I can remember from these errors and issues, the fix, the migration scripts and some information to collect permissions from sharepoint.
Success and Errors
While manually testing the migration we got these success and failure messages.
This is what we got if successful:
PS C:\> stsadm -o migrateuser -oldlogin OLDDOM\sjoerd -newlogin NEWDOM\sjoerd Operation completed successfully.
This is what we got if the command failed:
PS C:\> stsadm -o migrateuser -oldlogin OLDDOM\sjoerd -newlogin NEWDOM\sjoerd Cannot complete this action. Please try again.
If you use the migration script below the error code would be:
-2147467259
Setting Sharepoint Log Level
At first you should always set the loglevel to verbose to check if something obvious is going on.
This is how you set the loglevel to verbose:
=== logging level verbose PS C:\> stsadm -o setlogginglevel -tracelevel verbose
Ans this is how to set it back to default when you're done:
stsadm -o setlogginglevel -default
Logfiles can be found here:
c:\Program Files\Common Files\Microsoft Shared\web server extensions\12\LOGS
However, nothing useful would be found in the logfiles, no really specific related errors that would give a quick clue.
Fixing the Error
After some months of troubleshooting without real results and not being able to fix even one user we brought in Microsoft Tech Support on site. They found we never had any maintenance on our system, and that we had some orphaned sites. After deleting these orphaned sites (in test environment) some users migrated successfully. So we discussed a migration path including the cleanup os orphaned sites and ended up with a 99,9% success ratio. These are the steps we did to come to that success.
Pre Upgrade Check
First you have to run a preupgradecheck. This will do an orphaned sites check and will tell you (among other things) which sites are orphaned:
stsasm -o preupgradecheck
When the command is finished (might take a while) a webpage will be shown with all found information. At the bottom of the website the orphaned sites are shown. The page will be saved in this locations:
C\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\LOGS
The information you'll get will just show you the database name, but that is the information you need to run the enumallwebs option, which will tell you the site ids.
Enum All Webs
Now run this command to enumerate all sites in the found databases:
stsadm -o enumallwebs -databasename <database name> > c:\enumallwebs_dbname.txt
As said before, the databasename in the command is from the output of the preupgradecheck. The output you get from the enumallwebs command looks like this:
<Sites Count="3"> <Site Id="e04240bd-f716-4d1a-8485-001d940df0da" OwnerLogin="OLDDOM\user1" InSiteMap="False"> <Webs Count="1"> <Web Id="83eda8d1-a537-4a1f-b9af-af94631ad642" Url="/personal/user2" LanguageId="1033" TemplateName="SPSPERS#0" TemplateId="21" /> </Webs> </Site> <Site Id="d8acb129-1370-4d97-8045-002a1a921c2a" OwnerLogin="OLDDOM\user3" InSiteMap="True"> <Webs Count="1"> <Web Id="0319531c-631f-4e3c-b8a1-b73a0ebcdbfe" Url="/personal/user1" LanguageId="1033" TemplateName="SPSPERS#0" TemplateId="21" /> </Webs> </Site> <Site Id="362c7399-6d82-4a4a-aaaf-0048d42aa676" OwnerLogin="OLDDOM\user4" InSiteMap="True"> <Webs Count="1"> <Web Id="ef06e315-6c1f-4795-aa57-e4cfe92c56e6" Url="/personal/user5" LanguageId="1033" TemplateName="SPSPERS#0" TemplateId="21" /> </Webs> </Site> </Sites>
The information you need is in the Site Id line, if “InSiteMap” is “False” the site is orphaned and needs to be deleted. You need to collect all site id's for all orphaned sites from this output file, and delete them.
Delete Orphaned Site
Delete the found orphaned sites using this command:
stsadm -o deletesite -force -siteid <siteid in enumallwebs> -databaseserver <database server name> -databasename <database name> -force for example: stsadm -o deletesite -siteid e04240bd-f716-4d1a-8485-001d940df0da -databaseserver SPDB01 -databasename WSS_INTRANET -force
As mentioned before:
- Siteid is from the enumallwebs output
- Databasename is from the preupgradecheck
Cleanup Databases
Then you need to run a cleanup command to delete any old database that is around. If you want to can show these first:
- stsadm -o sync -listolddatabases <days>
- Use zero “0” days
- stsadm -o sync -deleteolddatabases <days>
- Use zero “0” days
Repeat
Now, from my experience you're not ready yet. You need to repeat these steps until the preupgradecheck says there are no more orphaned sites. This usually takes two times, I only had to run these steps three times.
Migration Script
When you have no more orphaned sites you can migrate all users with this script which will make a logfile of any failure you might still have:
# Sharepoint Script # Script variables $timestamp = Get-Date -format "yyyyMMdd-HH.mm" $startdir = "C:\" $importcsv = "$startdir\usermig.csv" $errorcsv = "$startdir\errorcsv-$timestamp.csv" # First, Get All AD users From CSV file $allusers = Import-Csv $importcsv # Create CSV file for error reporting # Define csv table $myTable = @() $teller = 1 ForEach ($importuser in $allusers){ $UserReporting = "" | Select OLDDOMUser,ReturnValue $currentuser = $importuser.samaccountname $newtime = Get-Date -format "yyyyMMdd-HH.mm" Write-Host "This is user $teller" Write-Host "$newtime : Now processing $currentuser" $UserReporting.OLDDOMUser = $currentuser STSADM -o migrateuser -oldlogin OLDDOM\$currentuser -newlogin NEWDOM\$currentuser $UserReporting.ReturnValue = $LastExitCode $teller ++ $myTable += $UserReporting } $myTable | Export-csv -NoTypeInformation $errorcsv
CSV Input File
SamAccountName user1 user2 user3 user4 user5 user6 user7
CSV Output File
"OLDDOMUser","ReturnValue" "user1","0" "user2","0" "user3","0" "user4","0" "user5","0" "user6","0" "user7","0"
Sharepoint Permissions
Even though you now have migrated all users, you haven't migrated any groups. There is no command to do that, this is a manual step in the migration. We found it useful to run a script to check for any permission that are assigned to groups (or users that haven't been migrated yet).
The Script
########################################################### #DisplaySPWebApp6.ps1 -URL <string> -searchScope <string> -userToFind <string> # #Author: Brian Jackett #Last Modified Date: Jan. 12, 2009 # #Supply Traverse the entire web app site by site to display # hierarchy and users with permissions to site. ########################################################### [void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint") #DECLARE VARIABLES [string]$siteUrl = $args[0] [string]$searchScope = $args[1] [string]$userToFind = $args[2] #DECLARE CONSTANTS $BUFFER_CHARS = " " function DetermineSpaceBuffer #-iterations <int> { [string]$spaceBuffer = "" for($i = 0; $i -lt $args[0]; $i++) {$spaceBuffer += $BUFFER_CHARS} return $spaceBuffer } #DECLARE FUNCTIONS function DrillDownADGroup #-group <[AD]DirectoryEntry> -depth <int> { [string]$spaceBuffer = DetermineSpaceBuffer $args[1] $domain = $args[0].Name.substring(0, $args[0].Name.IndexOf("\") + 1) $groupName = $args[0].Name.Remove(0, $args[0].Name.IndexOf("\") + 1) #BEGIN - CODE ADAPTED FROM SCRIPT CENTER SAMPLE CODE REPOSITORY #http://www.microsoft.com/technet/scriptcenter/scripts/powershell/search/users/srch106.mspx #GET AD GROUP FROM DIRECTORY SERVICES SEARCH $strFilter = "(&(objectCategory=Group)(name="+($groupName)+"))" $objDomain = New-Object System.DirectoryServices.DirectoryEntry $objSearcher = New-Object System.DirectoryServices.DirectorySearcher $objSearcher.SearchRoot = $objDomain $objSearcher.Filter = $strFilter # $colProplist = ("name","member") foreach ($i in $colPropList) { $catcher = $objSearcher.PropertiesToLoad.Add($i) } $colResults = $objSearcher.FindAll() #END - CODE ADAPTED FROM SCRIPT CENTER SAMPLE CODE REPOSITORY foreach ($objResult in $colResults) { foreach ($member in $objResult.Properties.member) { $indMember = [adsi] "LDAP://$member" #ATTEMPT TO GET AD OBJECT TYPE FOR USER, NOT WORKING RIGHT NOW #$user = $indMember.PSBase #$user.Properties $fullUserName = $domain + ($indMember.Name) DisplayADEntry $fullUserName ($args[1]) } } } function DisplaySPGroupMembers #-group <SPGroup> -depth <int> { [string]$spaceBuffer = DetermineSpaceBuffer $args[1] if($args[0].Users -ne $Null) { #START SHAREPOINT USERS ENTITY Write-Output $spaceBuffer"<SPUsers>" foreach($user in $args[0].Users) { DisplayADEntry $user ($args[1] + 1) } #END SHAREPOINT USERS ENTITY Write-Output $spaceBuffer"</SPUsers>" } } function DisplayADEntry #-user/group <SPUser> -depth <int> { #FILTER RESULTS IF LOOKING FOR SPECIFIC USER if($args[0].IsDomainGroup -eq "True") { $outputText = "$spaceBuffer$BUFFER_CHARS<Group>" + ($args[0]) Write-Output $outputText DrillDownADGroup $args[0] ($args[1]) $outputText = "$spaceBuffer$BUFFER_CHARS</Group>" Write-Output $outputText } else { #USER FOUND AS A CHILD OF AN EMBEDDED AD GROUP if(($userToFind -ne "" -and ($userToFind.ToUpper() -eq $args[0].LoginName.ToUpper() -or $userToFind.ToUpper() -eq $args[0].ToUpper())) -or $userToFind -eq "") { $outputText = "$spaceBuffer$BUFFER_CHARS<User>" + ($args[0]) + "</User>" Write-Output $outputText } } } function DetermineUserAccess #-web <SPWeb> -depth <int> { [string]$spaceBuffer = DetermineSpaceBuffer $args[1] #START SHAREPOINT GROUPS ENTITY Write-Output "$spaceBuffer<SPGroups>" foreach($perm in $args[0].Permissions) { #CHECK IF MEMBER IS AN ACTIVE DIRECTORY ENTRY OR SHAREPOINT GROUP if($perm.XML.Contains('MemberIsUser="True"') -eq "True") { DisplayADEntry $perm.Member ($args[1] + 1) } #IS A SHAREPOINT GROUP else { $outputText = "$spaceBuffer$BUFFER_CHARS<SPGroup>" + ($perm.Member) Write-Output $outputText DisplaySPGroupMembers $perm.Member ($args[1] + 2) Write-Output "$spaceBuffer$BUFFER_CHARS</SPGroup>" } } #END SHAREPOINT GROUPS ENTITY Write-Output "$spaceBuffer</SPGroups>" } function DisplayWebApplication #-webApp <SPWebApplication> { [string]$spaceBuffer = DetermineSpaceBuffer $args[1] #START WEB APPLICATION ENTITY $outputText = "$spaceBuffer<Web Application>" + ($args[0].Name) Write-Output $outputText if($args[0].Sites -ne $Null) { #START CONTAINED SITE COLLECTIONS ENTITY Write-Output "$spaceBuffer$BUFFER_CHARS<Site Collections>" foreach($spSiteColl in $args[0].Sites) { DisplaySiteCollection $spSiteColl ($args[1] + 2) $spSiteColl.Dispose() } #END CONTAINED SITE COLLECTIONS ENTITY Write-Output "$spaceBuffer$BUFFER_CHARS</SiteCollections>" } #END WEB APPLICATION ENTITY "$spaceBuffer</Web Application>" } function DisplaySiteCollection #-siteColl <SPSiteCollection> -depth <int> { [string]$spaceBuffer = DetermineSpaceBuffer $args[1] $sc = $args[0].OpenWeb() #START SITE COLLECTION ENTITY $outputText = "$spaceBuffer<Site Collection>" + ($sc.URL) Write-Output $outputText if($sc -ne $Null) { #START CONTAINED SITES ENTITY Write-Output "$spaceBuffer$BUFFER_CHARS<Sites>" foreach ($spWeb in $sc) { DisplayWeb $spWeb ($args[1] + 2) $spWeb.Dispose() } #END CONTAINED SITES ENTITY Write-Output "$spaceBuffer$BUFFER_CHARS</Sites>" } #END SITE COLLECTION ENTITY Write-Output "$spaceBuffer</Site Collection>" #CLEANUP SITE COLLECTION VARIABLE $sc.Dispose() } function DisplayWeb #-web <SPWeb> -depth <int> -parentWeb <SPWeb> { [string]$spaceBuffer = DetermineSpaceBuffer $args[1] #START SITE ENTITY $outputText = "$spaceBuffer<Site>" + ($args[0].URL) Write-Output $outputText if($args[0].HasUniquePerm -eq "True") { DetermineUserAccess $args[0] ($args[1] + 1) } else { Write-Output "$spaceBuffer<!--Inherits from parent>" } if($args[0].Webs -ne $Null) { #START CONTAINED SUBSITES ENTITY Write-Output "$spaceBuffer$BUFFER_CHARS<Subsites>" #RECURSIVELY SEARCH SUBWEBS foreach ($spSubWeb in $args[0].Webs) { DisplayWeb $spSubWeb ($args[1] + 2) $spSubWeb.Dispose() } #END CONTAINED SUBSITES ENTITY Write-Output "$spaceBuffer$BUFFER_CHARS</Subsites>" } #END SITE ENTITY Write-Output "$spaceBuffer</Site>" } function DisplayMissingParametersMessage { #Write-Output "You are missing a parameter for 'Site URL'" $script:siteURL = Read-Host "Enter Site URL" } ############ # MAIN ############ #IF MISSING PARM FOR SITE URL, ASK FOR INPUT TO FILL if($args.length -eq 0) { DisplayMissingParametersMessage } $rootSite = New-Object Microsoft.SharePoint.SPSite($siteUrl) $spWebApp = $rootSite.WebApplication Write-Output "<Web Applications>" #IF SEARCH SCOPE SPECIFIED FOR SITE, ONLY SEARCH SITE if($searchScope -eq "-site") { DisplaySiteCollection $rootSite 1 } #ELSE SEARCH ENTIRE WEB APP else { DisplayWebApplication $spWebApp 1 } Write-Output "</Web Applications>" #CLEANUP $rootSite.Dispose()
Starting and Redirection
The script should be started on your SharePoint management server. Also, output is directly written to the screen. You can redirect the output to a textfile, but then it doesnt't accept the requested parameters. You can however redirect the output and let the script request the url to search:
PS C:\> .\SP_Display-WebApp6.ps1 > permissions.txt Enter Site URL: http://intranetsite