|
| 1 | +<# |
| 2 | + .SYNOPSIS |
| 3 | + Synchronises Orchestrator users with Windows Active Directory, based on AD group membership mapped to Orchestrator Roles. |
| 4 | + .DESCRIPTION |
| 5 | + You provide the AD domain name and a mapping from relevand AD groups to Orchestrator Roles. |
| 6 | + New users in AD are added to Orchestrator and existing users added moved to the correct Role. |
| 7 | + The script also handles removing Orchestrator users from roles when they were removed from the corresponding AD group. |
| 8 | + AD users that were removed from all relevant AD groups (eg. an employee that changed role) or were removed from AD (eg. a former employee that left the company) become 'orphaned users'. They are still defined in Orchestrator but do not have any Role. The script supports the -OrphanedUsersAction parameter allowing to optionally List or Remove these users. |
| 9 | + The script is idempotent, repeated invocations should not modify the Orchestrator users unless something changed in AD. |
| 10 | + You should first import the UiPath.PowerShell module and authenticate yourself with your Orchestrator using Get-UiPathAuthToken before running this script. |
| 11 | + .PARAMETER DomainName |
| 12 | + The domain to sync users with. It does not necessarily has to be your current user or machine domain, but there must be some trust relationship so your Windows session can discover and interogate this domain AD. |
| 13 | + .PARAMETER RolesMapping |
| 14 | + A Hashtable mapping AD groups to Orchestrator roles. Make sure you type the names correctly. |
| 15 | + .PARAMETER OrphanedUsersAction |
| 16 | + Optional action to handle orphaned users. You can List or Remove these users. |
| 17 | + .EXAMPLE |
| 18 | + Sync-UiPathADUsers MyDomain @{'RPA Admins' = 'Administrator'; 'RPA Users' = 'User'} |
| 19 | + Import AD users from MyDomain and maps the members of the 'RPA Admins' AD group to the 'Administrator' Orchestrator role and members of the 'RPA Users' AD group to the 'User' Orchestrator role. |
| 20 | + .EXAMPLE |
| 21 | + Sync-UiPathADUsers MyDomain @{} -OrphanedUsersAction Remove |
| 22 | + Import AD users from MyDomain but since there is no mapping, the effect is to orphan all exiting Orchestrator MyDomain users and then remove them because of the -OrphanedUsersAction Remove parameter. In effect this invocation removes all MyDomain users from Orchestrator. |
| 23 | +#> |
| 24 | +param( |
| 25 | + [Parameter(Mandatory=$true, Position=0)] |
| 26 | + [string] $DomainName, |
| 27 | + [Parameter(Mandatory=$true, Position=1)] |
| 28 | + [HashTable] $RolesMapping, |
| 29 | + [Parameter(Mandatory=$false)] |
| 30 | + [ValidateSet('Remove', 'List', 'Ignore')] |
| 31 | + [string] $OrphanedUsersAction = 'Ignore' |
| 32 | +) |
| 33 | + |
| 34 | +$ErrorActionPreference = "Stop" |
| 35 | + |
| 36 | +function Get-ADGroupUser { |
| 37 | + param( |
| 38 | + [Parameter(Mandatory=$true, Position=0)] $dc, |
| 39 | + [Parameter(Mandatory=$true, Position=1)] $adGroup |
| 40 | + ) |
| 41 | + |
| 42 | + Write-Verbose "Get-ADGroupUser $adGroup" |
| 43 | + |
| 44 | + $users = @() |
| 45 | + $members = Get-ADGroupMember -Server $dc.PDCEmulator -Identity $adGroup |
| 46 | + |
| 47 | + foreach($member in $members) |
| 48 | + { |
| 49 | + if ($member.objectClass -eq 'user') |
| 50 | + { |
| 51 | + $users += @($member.SamAccountName) |
| 52 | + } |
| 53 | + elseif ($member.objectClass -eq 'group') |
| 54 | + { |
| 55 | + $childUsers = Get-ADGroupUser $dc $member.SamAccountName |
| 56 | + $users += $childUsers |
| 57 | + } |
| 58 | + } |
| 59 | + $users |
| 60 | +} |
| 61 | + |
| 62 | +try |
| 63 | +{ |
| 64 | + $operationSteps = @( |
| 65 | + "Validate Orchestrator role names", ` |
| 66 | + "Extracting AD group members", ` |
| 67 | + "Extracting Orchestrator users", ` |
| 68 | + "Preparing sync operations", ` |
| 69 | + "Importing New AD Users", ` |
| 70 | + "Modifying user role membership", ` |
| 71 | + "Process orphaned users" |
| 72 | + ) |
| 73 | + |
| 74 | + $idxOperationStep = 0 |
| 75 | + Write-Progress -Activity "Sync Orchestrator AD Users" ` |
| 76 | + -CurrentOperation $operationSteps[$idxOperationStep] ` |
| 77 | + -PercentComplete ($idxOperationStep/$operationSteps.Count*100) |
| 78 | + $i = 1 |
| 79 | + foreach($roleName in $RolesMapping.Values) |
| 80 | + { |
| 81 | + Write-Progress -Id 1 ` |
| 82 | + -Activity $operationSteps[$idxOperationStep] ` |
| 83 | + -CurrentOperation $roleName ` |
| 84 | + -PercentComplete ($i/$RolesMapping.Values.Count*100) |
| 85 | + $i += 1 |
| 86 | + $role = Get-UiPathRole -Name $roleName |
| 87 | + if ($role -eq $null) |
| 88 | + { |
| 89 | + throw "Could not find the Orchestrator role name: $roleName" |
| 90 | + } |
| 91 | + Write-Verbose "Role ok: $roleName $($role.Name)" |
| 92 | + } |
| 93 | + |
| 94 | + Write-Verbose "Get-ADDomain $DomainName" |
| 95 | + $dc = Get-ADDomain -Identity $DomainName |
| 96 | + |
| 97 | + $idxOperationStep += 1 |
| 98 | + Write-Progress -Activity "Sync Orchestrator AD Users" ` |
| 99 | + -CurrentOperation $operationSteps[$idxOperationStep] ` |
| 100 | + -PercentComplete ($idxOperationStep/$operationSteps.Count*100) |
| 101 | + |
| 102 | + [HashTable] $allADUsers = @{} |
| 103 | + |
| 104 | + $i = 1 |
| 105 | + foreach($adGoupName in $RolesMapping.Keys) |
| 106 | + { |
| 107 | + Write-Progress -Id 1 ` |
| 108 | + -Activity $operationSteps[$idxOperationStep] ` |
| 109 | + -PercentComplete ($i/$RolesMapping.Keys.Count *100) ` |
| 110 | + -CurrentOperation $roleMap |
| 111 | + $i += 1 |
| 112 | + |
| 113 | + $mappedRole = $RolesMapping[$adGoupName] |
| 114 | + |
| 115 | + $adGroupMembers = Get-ADGroupUser $dc $adGoupName | sort -Unique |
| 116 | + |
| 117 | + foreach($adGroupMember in $adGroupMembers) |
| 118 | + { |
| 119 | + $userName = $DomainName + '\' + $adGroupMember |
| 120 | + $adUser = $allADUsers[$userName] |
| 121 | + if ($adUser -eq $null) |
| 122 | + { |
| 123 | + $adUser = @{ roles = @(); name = $adGroupMember.Name } |
| 124 | + $null = $allADUsers.Add($userName, $adUser) |
| 125 | + } |
| 126 | + Write-Verbose "Discovered AD user $userName with role $mappedRole" |
| 127 | + $adUser.roles += @($mappedRole) |
| 128 | + } |
| 129 | + } |
| 130 | + |
| 131 | + $idxOperationStep += 1 |
| 132 | + Write-Progress -Activity "Sync Orchestrator AD Users" ` |
| 133 | + -CurrentOperation $operationSteps[$idxOperationStep] ` |
| 134 | + -PercentComplete ($idxOperationStep/$operationSteps.Count*100) |
| 135 | + $orchestratorUsers = Get-UiPathUser -Type User | Select UserName, Id, RolesList | where {$_.UserName.StartsWith($DomainName + '\', [System.StringComparison]::OrdinalIgnoreCase)} |
| 136 | + |
| 137 | + $idxOperationStep += 1 |
| 138 | + Write-Progress -Activity "Sync Orchestrator AD Users" ` |
| 139 | + -CurrentOperation $operationSteps[$idxOperationStep] ` |
| 140 | + -PercentComplete ($idxOperationStep/$operationSteps.Count*100) |
| 141 | + |
| 142 | + [HashTable] $operations = @{} |
| 143 | + |
| 144 | + # Copy all Orchestrator users |
| 145 | + foreach($orchestratorUser in $orchestratorUsers) |
| 146 | + { |
| 147 | + $op = @{id = $orchestratorUser.Id; isNew = $false; existingRoles = $orchestratorUser.RolesList; newRoles = @()} |
| 148 | + $null = $operations.Add($orchestratorUser.UserName, $op) |
| 149 | + } |
| 150 | + |
| 151 | + # Apply all AD users |
| 152 | + foreach($adUserName in $allADUsers.Keys) |
| 153 | + { |
| 154 | + $op = $operations[$adUserName] |
| 155 | + $adUser = $allADUsers[$adUserName] |
| 156 | + if ($op -eq $null) |
| 157 | + { |
| 158 | + $op = @{isNew = $true; adUser = $adUser} |
| 159 | + $operations.Add($adUserName, $op) |
| 160 | + } |
| 161 | + else |
| 162 | + { |
| 163 | + $op.newRoles += $adUser.roles |
| 164 | + } |
| 165 | + } |
| 166 | + |
| 167 | + |
| 168 | + # Figure add/remove list for each group and the new users that need to be added |
| 169 | + |
| 170 | + $newUsers = @() |
| 171 | + $orphanedUsers = @() |
| 172 | + $changedRoles = @{} |
| 173 | + foreach($adUserName in $operations.Keys) |
| 174 | + { |
| 175 | + $op = $operations[$adUserName] |
| 176 | + if ($op.isNew -eq $true) |
| 177 | + { |
| 178 | + $newUsers += @{userName = $adUserName; name = $op.adUser.name; roles = $op.adUser.roles} |
| 179 | + } |
| 180 | + else |
| 181 | + { |
| 182 | + $idxExisting = 0 |
| 183 | + $idxNew = 0 |
| 184 | + |
| 185 | + $existingRoles = $op.existingRoles | sort -Unique |
| 186 | + $newRoles = $op.newRoles | sort -Unique |
| 187 | + |
| 188 | + # Oh, Powershell.... |
| 189 | + if ($existingRoles -isnot [array]) |
| 190 | + { |
| 191 | + $existingRoles = @($existingRoles) |
| 192 | + } |
| 193 | + |
| 194 | + if ($newRoles -isnot [array]) |
| 195 | + { |
| 196 | + $newRoles = @($newRoles) |
| 197 | + } |
| 198 | + |
| 199 | + # because we sorted the two arrays we can use a merge algorithm |
| 200 | + |
| 201 | + while($idxExisting -lt $existingRoles.Count -or $idxNew -lt $newRoles.Count) |
| 202 | + { |
| 203 | + $existingRole = $null |
| 204 | + $newRole = $null |
| 205 | + $changedRole = $null |
| 206 | + $addOrRemove = $null |
| 207 | + $roleName = $null |
| 208 | + |
| 209 | + if ($idxExisting -lt $existingRoles.Count) |
| 210 | + { |
| 211 | + $existingRole = $existingRoles[$idxExisting] |
| 212 | + } |
| 213 | + |
| 214 | + if ($idxNew -lt $newRoles.Count) |
| 215 | + { |
| 216 | + $newRole = $newRoles[$idxNew] |
| 217 | + } |
| 218 | + |
| 219 | + if ($existingRole -eq $newRole) |
| 220 | + { |
| 221 | + # unchanged role, nothing to see here, move along |
| 222 | + $idxExisting += 1 |
| 223 | + $idxNew += 1 |
| 224 | + continue |
| 225 | + } |
| 226 | + elseif ($newRole -eq $null -or (($existingRole -ne $null) -and ($existingRole -lt $newRole))) |
| 227 | + { |
| 228 | + # user must be removed from this existing role |
| 229 | + $roleName = $existingRole |
| 230 | + $idxExisting += 1 |
| 231 | + $addOrRemove = 'remove' |
| 232 | + } |
| 233 | + else |
| 234 | + { |
| 235 | + # user must be added to this new role |
| 236 | + $roleName = $newRole |
| 237 | + $idxNew += 1 |
| 238 | + $addOrRemove = 'add' |
| 239 | + } |
| 240 | + $changedRole = $changedRoles[$roleName] |
| 241 | + if ($changedRole -eq $null) |
| 242 | + { |
| 243 | + $changedRole = @{addedUsers=@();removedUsers=@()} |
| 244 | + $changedRoles.Add($roleName, $changedRole) |
| 245 | + } |
| 246 | + if ($addOrRemove -eq 'add') |
| 247 | + { |
| 248 | + $changedRole.addedUsers += @($op.Id) |
| 249 | + } |
| 250 | + else |
| 251 | + { |
| 252 | + $changedRole.removedUsers += @($op.Id) |
| 253 | + } |
| 254 | + } |
| 255 | + |
| 256 | + if ($newRoles.Count -eq 0) |
| 257 | + { |
| 258 | + Write-Verbose "Is orphaned: $adUserName" |
| 259 | + $orphanedUsers += @{userName = $adUserName; id = $op.Id} |
| 260 | + } |
| 261 | + } |
| 262 | + } |
| 263 | + |
| 264 | + $idxOperationStep += 1 |
| 265 | + Write-Progress -Activity "Sync Orchestrator AD Users" ` |
| 266 | + -CurrentOperation $operationSteps[$idxOperationStep] ` |
| 267 | + -PercentComplete ($idxOperationStep/$operationSteps.Count*100) |
| 268 | + |
| 269 | + $i = 0 |
| 270 | + foreach($newUser in $newUsers) |
| 271 | + { |
| 272 | + $i += 1 |
| 273 | + |
| 274 | + Write-Progress -Id 1 ` |
| 275 | + -Activity $operationSteps[$idxOperationStep] ` |
| 276 | + -CurrentOperation $newUser.userName ` |
| 277 | + -PercentComplete ($i/$newUsers.Count * 100) |
| 278 | + |
| 279 | + Write-Verbose "Add-UiPathUser -Username $newUser.userName -Name $($newUser.name) -RolesList $($newUser.roles)" |
| 280 | + $null = Add-UiPathUser -Username $newUser.userName -Name $newUser.name -RolesList $newUser.roles |
| 281 | + } |
| 282 | + |
| 283 | + $idxOperationStep += 1 |
| 284 | + Write-Progress -Activity "Sync Orchestrator AD Users" ` |
| 285 | + -CurrentOperation $operationSteps[$idxOperationStep] ` |
| 286 | + -PercentComplete ($idxOperationStep/$operationSteps.Count*100) |
| 287 | + |
| 288 | + $i = 0 |
| 289 | + foreach($changedRole in $changedRoles.Keys) |
| 290 | + { |
| 291 | + $i += 1 |
| 292 | + $op = $changedRoles[$changedRole] |
| 293 | + |
| 294 | + Write-Progress -Id 1 ` |
| 295 | + -Activity $operationSteps[$idxOperationStep] ` |
| 296 | + -CurrentOperation $changedRole ` |
| 297 | + -PercentComplete ($i/$changedRoles.Keys.Count * 100) |
| 298 | + |
| 299 | + Write-Verbose "Get-UiPathRole -Name $changedRole" |
| 300 | + $role = Get-UiPathRole -Name $changedRole |
| 301 | + |
| 302 | + Write-Verbose "Edit-UiPathRoleUser $changedRole -Add $($op.addedUsers) -Remove $($op.removedUsers)" |
| 303 | + $null = Edit-UiPathRoleUser $role -Add $op.addedUsers -Remove $op.removedUsers |
| 304 | + } |
| 305 | + |
| 306 | + $idxOperationStep += 1 |
| 307 | + Write-Progress -Activity "Sync Orchestrator AD Users" ` |
| 308 | + -CurrentOperation $operationSteps[$idxOperationStep] ` |
| 309 | + -PercentComplete ($idxOperationStep/$operationSteps.Count*100) |
| 310 | + |
| 311 | + switch($OrphanedUsersAction) |
| 312 | + { |
| 313 | + 'Remove' |
| 314 | + { |
| 315 | + $i = 1 |
| 316 | + foreach($orphanedUser in $orphanedUsers) |
| 317 | + { |
| 318 | + Write-Progress -Id 1 ` |
| 319 | + -Activity "Remove orphaned users" ` |
| 320 | + -CurrentOperation $orphanedUser.userName ` |
| 321 | + -PercentComplete ($i/$orphanedUsers.Count * 100) |
| 322 | + $i += 1 |
| 323 | + Write-Verbose "Remove-UiPathUser -Id $($orphanedUser.id)" |
| 324 | + Remove-UiPathUser -Id $orphanedUser.id |
| 325 | + } |
| 326 | + } |
| 327 | + 'List' |
| 328 | + { |
| 329 | + foreach($orphanedUser in $orphanedUsers) |
| 330 | + { |
| 331 | + Write-Host $orphanedUser.userName |
| 332 | + } |
| 333 | + } |
| 334 | + 'Ignore' |
| 335 | + { |
| 336 | + # No-op |
| 337 | + } |
| 338 | + } |
| 339 | +} |
| 340 | +catch |
| 341 | +{ |
| 342 | + $e = $_.Exception |
| 343 | + $klass = $e.GetType().Name |
| 344 | + $line = $_.InvocationInfo.ScriptLineNumber |
| 345 | + $script = $_.InvocationInfo.ScriptName |
| 346 | + $msg = $e.Message |
| 347 | + |
| 348 | + Write-Error "$klass $msg ($script $line)" |
| 349 | +} |
| 350 | + |
| 351 | + |
| 352 | + |
| 353 | + |
| 354 | + |
| 355 | + |
0 commit comments