Migrace Microsoft SharePoint mezi dvěma Active Directory foresty

Hezký den,

po kratší době opět přinášíme jeden příběh „ze života“ – v tomto případě týkající se požadavku na přesun Microsoft SharePoint farmy (cca. 8 serverů) z jednoho Active Directory forestu do jiného. Resp. zjednodušeně řečeno z jedné do druhé domény.

Jednalo se o přenos prostředí našeho stálého zákazníka, který s naší podporou prošel od roku 2016 upgradem z Microsoft SharePoint 2010 až po Microsoft SharePoint 2019. Tudíž, dalo by se říci, poměrně známé prostředí. Co bohužel známé nebylo, tak stav, struktura a hlavně možnosti týkající managementu cílové Active Directory na straně zákazníka. Přeci jen cílem přenosu byla „velká“ korporátní AD infrastruktura, kde jsou možnosti managementu jednotlivých decentralizovaných IT oddělení „osekány“ na minimum. Podstatná informace zde však byla – mezi doménami neexistuje trust a nepočítá se s migrací jakýchkoliv objektů ze zdrojové AD infrastruktury do cílové korporátní (ano čtete správně). Zkrátka, těm cca. 400 uživatelům vzniknou v cílové doméně zcela nové účty, hesla, atd. To se nás týkalo pouze z části (myslím ty uživatelské účty), horší to bylo s Active Directory skupinami. V rámci naší vlastní metodiky používáme pro řízení přístupu do prostředí Microsoft SharePoint výhradně AD skupiny, které jsou na příslušných webech, podwebech, knihovnách, listech, či v konfigurační položkách (rolích) vždy přiřazeny s určitou úrovní oprávnění a centrálním „správním“ bodem pro přístup do příslušných agend (listů a knihoven) je řízen na úrovni AD a tudíž není třeba používat built-in skupin, které SharePoint poskytuje. Což je mimochodem stav, který je, co se týče udržení organizace přístupu peklo pro každého administrátora. O tom ale raději někdy jindy.

Ale zpět k tématu. Na oficiálních i neoficiálních zdrojích se dá najít bezpočet příkladů a návodů, jak lze takovou proceduru provést, jak se říká „bez ztráty kytičky“. No, nerad bych se v každém příspěvku opakoval (asi si domyslíte sami, jaký to mělo přínos) ale jediným využitelným příkazem v rámci skriptů, které bylo potřeba pro takovou migraci připravit byl tento. Abychom zákazníkovy přinesli v rámci této akce ještě nějakou přidanou hodnotu, rozhodli jsme se, že v rámci přenosu provedeme ještě upgrade SharePoint z verze 2016 na verzi 2019. Jen, co se velikosti týče, tak zákazník má data ložena jak v SQL Blob resp. SQL FileStream, kde se nachází cca. 2TB dat, tak pochopitelně v contentové databázi s nějakou zanedbatelnou velikostí okolo 300GB. Tudíž z toho pohledu jsou všechny operace velmi zdlouhavé a je ideální pokud se vše zadaří tj. na první dobrou :-). Což se naštěstí podařilo. Pochopitelně takovéto akci předcházela příprava v podobě vývoje skriptů pro migraci uživatelů a skupin (zákazník měl již z dřívější doby k dispozici mapovací tabulku uživatelů ze staré AD do nové AD – hurá!), jedné testovací migrace na UAT prostředí a pak směle do toho. Kdybych měl nastínit proces, kterým se podařilo provést migraci SharePoint farmy v rámci domén, vypadalo by to asi následovně:

Doba migrace – 63 hodin. No jo!!! Kamarádi! To stálo za to. Na pokraji smrti mrazem, na pokraji smrti hladem, na pokraji smrti vysílením, ale stálo to za to! …jak praví klasik. U dalších dílů našich postřehů se na dočtenou těší Jan Hanuš

…abych nezapomněl, pro inspiraci přikládám slibovaný skript na migraci uživatelů – třeba se někomu z vás bude hodit 😉

<# .SYNOPSIS Migrates AD Users .DESCRIPTION Migrates AD Users #>
param (
    [Parameter(Mandatory = $True)]
    [string] $WebUrl,	
    [Parameter(Mandatory = $True)]
    [string] $MapFile,	
    [bool] $DryRun = 0
)
Add-PSSnapin microsoft.sharepoint.powershell
# Check configuration
$_regVal_ScriptsSource = Get-ItemProperty -Path HKLM:\SOFTWARE\EXPINIT\Scripts -Name "ScriptsSource" -ErrorAction SilentlyContinue
$_regVal_LocalDirectory = Get-ItemProperty -Path HKLM:\SOFTWARE\EXPINIT\Scripts -Name "LocalDirectory" -ErrorAction SilentlyContinue
if (($null -eq $_regVal_ScriptsSource) -or ($null -eq $_regVal_LocalDirectory)) {
    Write-Host -ForegroundColor Red "Invalid Scrips configuration! Exiting."; exit 1
}
# Includes
$global:EXPScriptStartDir = Split-Path (Get-Variable MyInvocation).Value.MyCommand.Path
if (Test-Path ($global:EXPScriptStartDir + '\EXPINIT.Core.ps1')) {
    . ($global:EXPScriptStartDir + '\EXPINIT.Core.ps1')
}
ElseIf (Test-Path ($_regVal_LocalDirectory.LocalDirectory + '\EXPINIT.Core.ps1')) {
    . ($_regVal_LocalDirectory.LocalDirectory + '\EXPINIT.Core.ps1')
}
ElseIf (Test-Path ($_regVal_ScriptsSource.ScriptsSource + '\EXPINIT.Core.ps1')) {
    . ($_regVal_ScriptsSource.ScriptsSource + '\EXPINIT.Core.ps1')
}
Else {
    Write-Host -ForegroundColor Red "Cannot find EXPINIT.Core.ps1!"
    exit -1
}
# Configuration
$str_LogFile = "C:\Temp\MigrateADUsers.txt"
$int_LogSize = 10485760
$int_LogDays = 360
$str_SPClaimPrefix = "i:0#.w|"
# Main
try {
    $web = Get-SPWeb $WebUrl
    if ($web -ne $null) {
        Write-Log -Message "Connecting to SP Web $($web)" -Type Info -LogFile $str_LogFile -LogRotateSize $int_LogSize -LogRotateKeepDays $int_LogDays
        if (Test-Path $MapFile) {	
            Write-Log -Message "Trying to import mapping file $($MapFile)" -Type Info -LogFile $str_LogFile -LogRotateSize $int_LogSize -LogRotateKeepDays $int_LogDays
            $arr_ADUsersMapping = Import-Csv -Path $MapFile
            if ($arr_ADUsersMapping.Count -gt 0) {
                Write-Log -Message "Import file $($MapFile) successfully loaded to memory" -Type Info -LogFile $str_LogFile -LogRotateSize $int_LogSize -LogRotateKeepDays $int_LogDays
                foreach ($arrADUserMember in $arr_ADUsersMapping) {
                    Write-Log -Message "Starting mapping procedure for pair $($arrADUserMember.UserNameOldDomain) - $($arrADUserMember.UserNameNewDomain)" -Type Info -LogFile $str_LogFile -LogRotateSize $int_LogSize -LogRotateKeepDays $int_LogDays                
                    Write-Log -Message "Ensuring old user $($arrADUserMember.UserNameOldDomain)" -Type Info -LogFile $str_LogFile -LogRotateSize $int_LogSize -LogRotateKeepDays $int_LogDays                                                        
                    $obj_OldUser = $web.EnsureUser($str_SPClaimPrefix + $arrADUserMember.UserNameOldDomain)
                    if ($obj_OldUser -ne $null) {
                        Write-Log -Message "Old user $($arrADUserMember.UserNameOldDomain) successfully ensured: $($obj_OldUser.Name) / $($obj_OldUser.LoginName) " -Type Info -LogFile $str_LogFile -LogRotateSize $int_LogSize -LogRotateKeepDays $int_LogDays                
                        Write-Log -Message "Moving old user $($arrADUserMember.UserNameOldDomain) to $($arrADUserMember.UserNameNewDomain)" -Type Info -LogFile $str_LogFile -LogRotateSize $int_LogSize -LogRotateKeepDays $int_LogDays
                        if ($DryRun -eq $False) {                                             
                            $str_MPUser = $str_SPClaimPrefix + $arrADUserMember.UserNameNewDomain       
                            if ($str_MPUser -ne $null) {
                                Move-SPUser -Identity $obj_OldUser -NewAlias $str_MPUser -IgnoreSID -Confirm:$false                            
                                Write-Log -Message "User $($obj_OldUser.Name) / $($obj_OldUser.LoginName) migrated to $($arrADUserMember.UserNameNewDomain)" -Type Info -LogFile $str_LogFile -LogRotateSize $int_LogSize -LogRotateKeepDays $int_LogDays
                            }
                        }
                        if ($DryRun -eq $True) {
                            Write-Log -Message "User $($obj_OldUser.Name) / $($obj_OldUser.LoginName) migrated to $($arrADUserMember.UserNameNewDomain)" -Type Info -LogFile $str_LogFile -LogRotateSize $int_LogSize -LogRotateKeepDays $int_LogDays
                            Write-Log -Message "This was just a DRY RUN. NO changes has been made." -Type Info -LogFile $str_LogFile -LogRotateSize $int_LogSize -LogRotateKeepDays $int_LogDays						
                        }
                    }
                }                    
            }                
        }            
    }
   
}
catch {
    Write-Log -Message "Migration operation failed!" -Type Error -LogFile $str_LogFile -LogRotateSize $int_LogSize -LogRotateKeepDays $int_LogDays	
}