This post is provided by App Dev Manager, Steve Keeler who outlines an approach for performing TFS migrations to a new data center in a different domain and where there is limited or no connectivity between the current and target environments.
Applies to: TFS 2013 | TFS 2015 | TFS 2017 – This article outlines an approach for performing TFS migrations to a new data center (in a different domain) where there is limited or no connectivity between the current and target environments. In this context, migrating the TFS deployment includes restoring backups of the TFS (config & collections), SharePoint (content), and SSRS (reports) databases in the new domain environment. If you are faced with this scenario, read on. Even if this does not match your scenario, some parts of the information covered here may still be useful when dealing with large numbers of changes to existing TFS, SharePoint, and SSRS user identities and permissions.
Background
There are two documented types of moves you can perform on Team Foundation Server: hardware and environment. In a hardware move, you are moving (or cloning) an existing TFS implementation to a new set of servers in the same domain. In an environment move, you are changing the domain of the TFS deployment. Documentation for both types of moves is available here:
- Move or Clone Team Foundation Server from one hardware to another
- Move Team Foundation Server from one environment to another
Both of these articles have the following important note:
“In some situations you might want to change the domain of a TFS deployment as well as its hardware. Changing the domain is an environment-based move, and you should never combine the two move types. First complete the hardware move, and then change the environment.”
But what if your migration is to a new data center or servers located in a different domain, with restricted or no connectivity to the original domain? This scenario can occur when changing data centers, for example during mergers of separate organizations or transfer of responsibility for services. With limited or no connectivity between environments, it is not possible to join the existing TFS deployment to the new domain. In this case, the hardware move is the closer match for what we need to achieve. Following the instructions in the hardware move documentation gets us through most of the migration process. After the hardware move has been completed we will still need to remap user identities/permissions for TFS, SharePoint, and SSRS. A common element to each of these three areas is the mapping of your user identities from the old domain into the new domain. We’ll look at this requirement in the User Mapping File section below. One thing you should pay special attention to during the process is ensuring that any TFS user identities in the new domain that you will be mapping TFS users from the old domain into are not part of the Local Administrators group. This is important because the TFS identity synchronization service will automatically create TFS user identities for members of this group. Once a TFS user identity has been created in the new domain, it is not possible to map a TFS user identity from the old domain into it.
User Mapping File
To support the user identity and permission changes needed in the three identified areas, we will create a common user mapping text file. In this schema, OldDomain represents the name of the original domain, OldAccount represents the user account names in the original domain, NewDomain represents the name of the target domain, and NewAccount represents the user account names in the target domain. The following illustrates a user mapping file for a sample scenario where TFSDEV is the original domain and TFSPRD is the target domain:
OldDomain,OldAccount,NewDomain,NewAccount TFSDEV,Developer1,TFSPRD,Developer1 TFSDEV,Developer3,TFSPRD,Developer2
The first line of this file contains the attribute names, which must match exactly for the PowerShell scripts provided later to operate correctly. In this sample file, we are mapping the Developer1 account in the TFSDEV domain to the same account name in the TFSPRD domain. The Developer3 user account in the TFSDEV domain is mapped to Developer2 in the TFSPRD domain. We’ve chosen the CSV format for the user mapping file in this example as it is relatively easy to manage with a wide range of application tools and utilities. However, you could also use other formats, with the appropriate changes to the sample scripts. For example, JSON and XML are two possible alternatives. Once the user mapping file has been created, we can proceed with the changes detailed in the following three sections:
TFS Identities
Remapping TFS identities is accomplished using the TfsConfig Identities /change command. The TfsConfig.exe utility is installed with Team Foundation Server, under the \Tools folder of the installation path. If all (or most) of your account names match between the two domain environments, then you can run the command directly, omitting the /account and /toaccount parameters. Often times however, you will need to map many user accounts that have different names in each domain.
tfs-change-identities.ps1
The following PowerShell script illustrates how you can handle the more complex account mapping scenario, using the user mapping file created in the previous section. The $TfsConfig parameter in this sample script points to the location of the TfsConfig.exe application in a default installation of TFS 2015. If its location is different in your environment, you will need to either change the default value in the script, or provide the location as a parameter value when invoking the script.
[CmdletBinding()] param ( [Parameter(Mandatory=$true, Position=0)] [string] $UserMapFile, [Parameter(Position=1)] [string] $TfsConfig = "C:\Program Files\Microsoft Team Foundation Server 14.0\Tools\TfsConfig.exe" ) if (-Not (Test-Path $TfsConfig)) { Write-Error "TfsConfig.exe file does not exist in the specified location" return } if (-Not (Test-Path $UserMapFile)) { Write-Error "User map file does not exist in the specified location" return } $usermap = Import-Csv $UserMapFile foreach ($user in $usermap) { Write-Host "Changing $($user.OldDomain)\$($user.OldAccount)" -NoNewline Write-Host " to $($user.NewDomain)\$($user.NewAccount)" & $TfsConfig Identities /change ` /fromdomain:$($user.OldDomain) ` /todomain:$($user.NewDomain) ` /account:$($user.OldAccount) ` /toaccount:$($user.NewAccount) }
Running this script with the user mapping file created in the previous section displays the following output:
PS C:\> .\tfs-change-identities.ps1 c:\docs\usermap.csv Changing TFSDEV\Developer1 to TFSPRD\Developer1 Account Name Exists (see note 1) Matches (see note 2) TFSPRD\Developer1 True False Changed 1 security identifier(s) (SIDs) were changed in Team Foundation Server. == NOTES == (1) The Exists column indicates whether the listed account exists in Windows. For the List mode of the command, this is the account stored in Team Foundation Server. For the Change mode, it is the target of the change. (2) The Matches column indicates whether the SID stored in Team Foundation Server matches with Windows.
The script executes the TfsConfig Identities /change command for each entry in the user mapping file, and will display the Changed value on success. Changed TFS identities will not match the Windows user identities until the next identity synchronization has occurred – by default, this happens at the top of every hour. It is possible to force an identity synchronization to happen immediately by invoking a web service call.
SharePoint Identities
After moving your SharePoint content database for TFS to a new domain, the SharePoint user permissions still reference user identities in the original domain environment.
sp-change-identities.ps1
It is relatively easy to remap the SharePoint user identities by using the Move-SPUser PowerShell cmdlet. The following PowerShell script illustrates how you can use the previously created User Mapping File to remap the SharePoint user identities into the new domain environment.
[CmdletBinding()] param ( [Parameter(Mandatory=$true, Position=0)] [string] $WebAppUrl, [Parameter(Mandatory=$true, Position=1)] [string] $UserMapFile ) $ErrorActionPreference = "Stop" Add-PSSnapIn "Microsoft.SharePoint.Powershell" $moveErrors = $null $usermap = Import-Csv $UserMapFile $users = Get-SPWebApplication $WebAppUrl | Get-SPSite | Get-SPWeb | Get-SPUser foreach ($user in $users) { $parts = $user.UserLogin.Split('\') if ($parts.Count -ne 2) { continue } $domain = $parts[0] $account = $parts[1] foreach ($entry in $usermap) { if ($domain -eq $entry.OldDomain -and $account -eq $entry.OldAccount) { $oldAlias = $domain + "\" + $account $newAlias = $entry.NewDomain + "\" + $entry.NewAccount Write-Host "Changing SharePoint user from [" -ForegroundColor Yellow -NoNewline Write-Host $oldAlias "] to [" $newAlias "]" -ForegroundColor Yellow Move-SPUser -Identity $user -NewAlias $newAlias -IgnoreSID -ErrorAction SilentlyContinue } } }
Run the above script, supplying the URL of the SharePoint web application associated with the TFS SharePoint site collections, and location of the user mapping file. Typically you will run this on the SharePoint server in the new environment, which should already have the Microsoft.SharePoint.Powershell module present. Running this script produces the following sample output:
PS C:\> .\sp-change-identities.ps1 http://tfs-prd-at/ .\usermap.csv Changing SharePoint user from [TFSDEV\Developer1] to [TFSPRD\Developer1] Changing SharePoint user from [TFSDEV\Developer3] to [TFSPRD\Developer2]
After remapping your SharePoint user identities, login with a user in the new domain and navigate to a SharePoint site associated with one or more of your TFS team projects to validate that the SharePoint user was remapped successfully.
SSRS Permissions
The last area we’ll address is SQL Server Reporting Services (SSRS) permissions for your TFS team project reports. Managing SSRS user permissions (roles) can be done through the Report Manager web interface as documented in Grant user access to a report server, or via the command-line with the RS.exe utility. This section describes a PowerShell-based solution to simplify the replication of hundreds (or thousands) of distinct SSRS user permissions between different domain environments. We will once again leverage the User Mapping File already created, combined with two PowerShell scripts to: 1) retrieve existing user permissions in the original domain environment, and 2) reapply those permissions in the new domain environment.
ssrs-list-permissions.ps1
The first step is to generate a textual representation of user permissions in the original domain environment. It takes two parameters: the SSRS report server starting path, and an SSRS instance name if this is not a default SSRS instance. The script should be run on the SSRS server in the original domain environment. It can be run from elsewhere by changing the $reportServerUri variable to point to the SSRS Web Service URL (available in Reporting Services Configuration Manager).
<# PowerShell script to iterate over non-inherited SSRS permissions. 1\. Change the $sourceFolderPath value if you want to start at a lower level than root of SSRS. 2\. Change the $InstanceName if this is not the SSRS default instance #> [CmdletBinding()] param ( [Parameter(Position=0)] [string] $sourceFolderPath = "/", [Parameter(Position=1)] [string] $InstanceName = "" ) if ($InstanceName -eq "") { $reportServerUri = "http://localhost/ReportServer/ReportService2010.asmx" } else { $reportServerUri = "http://localhost/ReportServer_" + $InstanceName + "/ReportService2010.asmx" } $scriptDirectory = Split-Path $MyInvocation.MyCommand.Path $outfile = $scriptDirectory + "\ssrs-list-permissions.csv" Write-Host "SSRS web service: " $reportServerUri -ForegroundColor Yellow Write-Host "Working directory: " $scriptDirectory -ForegroundColor Yellow Write-Host "Computer name: " $env:COMPUTERNAME -ForegroundColor Yellow Write-Host "Logged in as: " $env:USERNAME -ForegroundColor Yellow $inheritParent = $true $output = @() $proxy = New-WebServiceProxy -UseDefaultCredential -Uri $reportServerUri $items = $proxy.ListChildren($sourceFolderPath, $true) ` | Select-Object TypeName, Path, Name foreach ($item in $items) { Add-Member -InputObject $item -MemberType NoteProperty -Name RoleName -Value '' Add-Member -InputObject $item -MemberType NoteProperty -Name FullyQualifiedUserName -Value '' Add-Member -InputObject $item -MemberType NoteProperty -Name Domain -Value '' Add-Member -InputObject $item -MemberType NoteProperty -Name UserName -Value '' $needHeader = $true foreach ($policy in $proxy.GetPolicies($item.path, [ref]$inheritParent)) { if ($inheritParent) { continue } if ($needHeader) { Write-Host Write-Host "Type: " $item.TypeName -ForegroundColor Yellow Write-Host "Path: " $item.Path -ForegroundColor Yellow Write-Host "Name: " $item.Name -ForegroundColor Yellow Write-Host "--------------------------------------------------" $needHeader = $false } Write-Host $policy.GroupUserName -NoNewline Write-Host " (" -NoNewline $needComma = $false foreach ($role in $policy.Roles) { if ($needComma) { Write-Host ", " -NoNewline } else { $needComma = $true } Write-Host $role.Name -ForegroundColor Green -NoNewline $temp = $item.PsObject.Copy(); $temp.RoleName = $role.Name $temp.FullyQualifiedUserName = $policy.GroupUserName; $temp.Domain = $policy.GroupUserName.Split('\')[0] $temp.UserName = $policy.GroupUserName.Split('\')[1] $output += $temp; $temp.reset; } Write-Host ")" } } $output | Export-csv $outfile -NoTypeInformation; Write-Host Write-Host "Completed with output stored in: " $outfile Write-Host
This script creates an output file named ssrs-list-permissions.csv in the same directory where the script resides. Remember the location and name of this output file as it will be needed in the next part of the process. Sample console output from running the ssrs-list-permissions.ps1 script looks like this:
PS C:\> .\ssrs-list-permissions.ps1 Type: Folder Path: /TfsReports/DefaultCollection/Agile Name: Agile TFSDEV\Developer1 (Content Manager, Publisher) TFSDEV\Developer3 (Browser) Completed with output stored in: ssrs-list-permissions.csv
The permissions output file has the following format:
TypeName,Path,Name,RoleName,FullyQualifiedUserName,Domain,UserName Folder,/TfsReports/DefaultCollection/Agile,Agile,Content Manager,TFSDEV\Developer1,TFSDEV,Developer1 Folder,/TfsReports/DefaultCollection/Agile,Agile,Publisher,TFSDEV\Developer1,TFSDEV,Developer1 Folder,/TfsReports/DefaultCollection/Agile,Agile,Browser,TFSDEV\Developer3,TFSDEV,Developer3
If a user is assigned to multiple roles, each role will be specified on a separate line in the permissions output file generated by the ssrs-list-permissions.ps1 script.
ssrs-change-permissions.ps1
The second step is to use the User Mapping File along with the permissions output file generated in the previous step. Together, these two files are used as input to the script below that is run on the SSRS server in the new domain environment. This script uses the SSRS web service to update permission policies. For each object/role permission identified in the the permissions output file generated in the original domain, it performs a lookup in the user mapping file to determine user identity in the new domain, and grants that identity the object/role permission.
<# The first parameter (ssrsPermissionsFile) is the file path to the output from the 'ssrs-list-permissions.ps1' script run in the old domain. The second parameter (UserMapFile) is the file path to the user mapping file. The third parameter (ssrsInstanceName) is the SSRS instance name. If running on the default SSRS instance, press <Enter> if prompted to supply an empty string value. #> [CmdletBinding()] param ( [Parameter(Mandatory=$true, Position=0)] [string] $ssrsPermissionsFile, [Parameter(Mandatory=$true, Position=1)] [string] $UserMapFile, [Parameter(Mandatory=$true, Position=2)] [AllowEmptyString()] [string] $ssrsInstanceName ) if ($ssrsInstanceName -eq "") { $reportServerUri = "http://localhost/ReportServer/ReportService2010.asmx" } else { $reportServerUri = "http://localhost/ReportServer_" + $ssrsInstanceName + "/ReportService2010.asmx" } if (-Not (Test-Path $ssrsPermissionsFile)) { Write-Error $ssrsPermissionsFile " file does not exist in the specified location" return } if (-Not (Test-Path $UserMapFile)) { Write-Error $UserMapFile " file does not exist in the specified location" return } Write-Host "Loading SSRS permissions (old domain) from file: " $ssrsPermissionsFile $permissions = Import-Csv $ssrsPermissionsFile Write-Host "Loading user mapping file: " $UserMapFile $usermap = Import-Csv $UserMapFile $lookup = @{} $usermap | % { $key = $_.OldDomain + "\" + $_.OldAccount; $lookup[$key] = $_ } $inheritParent = $true Write-Host "Connecting to SSRS web service: " $reportServerUri $proxy = New-WebServiceProxy -UseDefaultCredential -Uri $reportServerUri $type = $proxy.GetType().Namespace $policyType = "{0}.Policy" -f $type $roleType = "{0}.Role" -f $type foreach ($permission in $permissions) { $key = $permission.FullyQualifiedUserName $user = $lookup[$key] if ($user -ne $null) { $identity = $user.NewDomain + "\" + $user.NewAccount $message = "Retrieving policies for user '{0}' on '{1}'" -f $identity, $permission.Path Write-Host $message -ForegroundColor Yellow $policies = $proxy.GetPolicies($permission.Path, [ref]$inheritParent); $policy = $policies | where { $_.GroupUserName -eq $identity } | select -First 1 if (-not $policy) { $policy = New-Object ($policyType) $policy.GroupUserName = $identity $policy.Roles = @() $policies += $policy $message = "Creating policy for user: '{0}'" -f $identity Write-Host $message -ForegroundColor Cyan } $role = $policy.Roles | where { $_.Name -eq $permission.RoleName } | select -First 1 if (-not $role) { $role = New-Object ($roleType) $role.Name = $permission.RoleName $policy.Roles += $role $message = "Adding user '{0}' to role '{1}'" -f $identity, $permission.RoleName Write-Host $message -ForegroundColor Cyan } $message = "Saving policies for user '{0}' on '{1}'" -f $identity, $permission.Path Write-Host $message -ForegroundColor Cyan $proxy.SetPolicies($permission.Path, $policies); } else { $message = "'{0}' not found in user mapping file" -f $key Write-Host $message -ForegroundColor Red } }
Sample console output from running the ssrs-change-permissions.ps1 script looks like this:
PS C:\> C:\Scripts\ssrs-change-permissions.ps1 .\ssrs-list-permissions.csv .\usermap.csv "" Loading SSRS permissions (old domain) from file: ssrs-list-permissions.csv Loading user mapping file: usermap.csv Connecting to SSRS web service: http://localhost/ReportServer/ReportService2010.asmx Retrieving policies for user 'TFSPRD\Developer1' on '/TfsReports/DefaultCollection/Agile' Creating policy for user: 'TFSPRD\Developer1' Adding user 'TFSPRD\Developer1' to role 'Content Manager' Saving policies for user 'TFSPRD\Developer1' on '/TfsReports/DefaultCollection/Agile' Adding user 'TFSPRD\Developer1' to role 'Publisher' Saving policies for user 'TFSPRD\Developer1' on '/TfsReports/DefaultCollection/Agile' Retrieving policies for user 'TFSPRD\Developer2' on '/TfsReports/DefaultCollection/Agile' Creating policy for user: 'TFSPRD\Developer2' Adding user 'TFSPRD\Developer2' to role 'Browser' Saving policies for user 'TFSPRD\Developer2' on '/TfsReports/DefaultCollection/Agile'
Conclusion
We’ve demonstrated how you can leverage PowerShell scripting to simplify the process of managing user identities in a specific migration scenario for an enterprise-scale TFS deployment.
0 comments