PowerFederatedDirectory.psm1
function Join-UriQuery { <# .SYNOPSIS Provides ability to join two Url paths together including advanced querying .DESCRIPTION Provides ability to join two Url paths together including advanced querying which is useful for RestAPI/GraphApi calls .PARAMETER BaseUri Primary Url to merge .PARAMETER RelativeOrAbsoluteUri Additional path to merge with primary url (optional) .PARAMETER QueryParameter Parameters and their values in form of hashtable .EXAMPLE Join-UriQuery -BaseUri 'https://evotec.xyz/' -RelativeOrAbsoluteUri '/wp-json/wp/v2/posts' -QueryParameter @{ page = 1 per_page = 20 search = 'SearchString' } .EXAMPLE Join-UriQuery -BaseUri 'https://evotec.xyz/wp-json/wp/v2/posts' -QueryParameter @{ page = 1 per_page = 20 search = 'SearchString' } .EXAMPLE Join-UriQuery -BaseUri 'https://evotec.xyz' -RelativeOrAbsoluteUri '/wp-json/wp/v2/posts' .NOTES General notes #> [alias('Join-UrlQuery')] [CmdletBinding()] param ([parameter(Mandatory)][uri] $BaseUri, [parameter(Mandatory = $false)][uri] $RelativeOrAbsoluteUri, [Parameter()][System.Collections.IDictionary] $QueryParameter) if ($BaseUri -and $RelativeOrAbsoluteUri) { $Url = Join-Uri -BaseUri $BaseUri -RelativeOrAbsoluteUri $RelativeOrAbsoluteUri } else { $Url = $BaseUri } if ($QueryParameter) { $Collection = [System.Web.HttpUtility]::ParseQueryString([String]::Empty) foreach ($key in $QueryParameter.Keys) { $Collection.Add($key, $QueryParameter.$key) } } $uriRequest = [System.UriBuilder] $Url if ($Collection) { $uriRequest.Query = $Collection.ToString() } return $uriRequest.Uri.AbsoluteUri } function Remove-EmptyValue { [alias('Remove-EmptyValues')] [CmdletBinding()] param([alias('Splat', 'IDictionary')][Parameter(Mandatory)][System.Collections.IDictionary] $Hashtable, [string[]] $ExcludeParameter, [switch] $Recursive, [int] $Rerun, [switch] $DoNotRemoveNull, [switch] $DoNotRemoveEmpty, [switch] $DoNotRemoveEmptyArray, [switch] $DoNotRemoveEmptyDictionary) foreach ($Key in [string[]] $Hashtable.Keys) { if ($Key -notin $ExcludeParameter) { if ($Recursive) { if ($Hashtable[$Key] -is [System.Collections.IDictionary]) { if ($Hashtable[$Key].Count -eq 0) { if (-not $DoNotRemoveEmptyDictionary) { $Hashtable.Remove($Key) } } else { Remove-EmptyValue -Hashtable $Hashtable[$Key] -Recursive:$Recursive } } else { if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) { $Hashtable.Remove($Key) } } } else { if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) { $Hashtable.Remove($Key) } } } } if ($Rerun) { for ($i = 0; $i -lt $Rerun; $i++) { Remove-EmptyValue -Hashtable $Hashtable -Recursive:$Recursive } } } function Split-Array { <# .SYNOPSIS Split an array into multiple arrays of a specified size or by a specified number of elements .DESCRIPTION Split an array into multiple arrays of a specified size or by a specified number of elements .PARAMETER Objects Lists of objects you would like to split into multiple arrays based on their size or number of parts to split into. .PARAMETER Parts Parameter description .PARAMETER Size Parameter description .EXAMPLE This splits array into multiple arrays of 3 Example below wil return 1,2,3 + 4,5,6 + 7,8,9 Split-array -Objects @(1,2,3,4,5,6,7,8,9,10) -Parts 3 .EXAMPLE This splits array into 3 parts regardless of amount of elements Split-array -Objects @(1,2,3,4,5,6,7,8,9,10) -Size 3 .NOTES #> [CmdletBinding()] param([alias('InArray', 'List')][Array] $Objects, [int]$Parts, [int]$Size) if ($Objects.Count -eq 1) { return $Objects } if ($Parts) { $PartSize = [Math]::Ceiling($inArray.count / $Parts) } if ($Size) { $PartSize = $Size $Parts = [Math]::Ceiling($Objects.count / $Size) } $outArray = [System.Collections.Generic.List[Object]]::new() for ($i = 1; $i -le $Parts; $i++) { $start = (($i - 1) * $PartSize) $end = (($i) * $PartSize) - 1 if ($end -ge $Objects.count) { $end = $Objects.count - 1 } $outArray.Add(@($Objects[$start..$end])) } , $outArray } function Join-Uri { <# .SYNOPSIS Provides ability to join two Url paths together .DESCRIPTION Provides ability to join two Url paths together .PARAMETER BaseUri Primary Url to merge .PARAMETER RelativeOrAbsoluteUri Additional path to merge with primary url .EXAMPLE Join-Uri 'https://evotec.xyz/' '/wp-json/wp/v2/posts' .EXAMPLE Join-Uri 'https://evotec.xyz/' 'wp-json/wp/v2/posts' .EXAMPLE Join-Uri -BaseUri 'https://evotec.xyz/' -RelativeOrAbsoluteUri '/wp-json/wp/v2/posts' .EXAMPLE Join-Uri -BaseUri 'https://evotec.xyz/test/' -RelativeOrAbsoluteUri '/wp-json/wp/v2/posts' .NOTES General notes #> [alias('Join-Url')] [cmdletBinding()] param([parameter(Mandatory)][uri] $BaseUri, [parameter(Mandatory)][uri] $RelativeOrAbsoluteUri) return ($BaseUri.OriginalString.TrimEnd('/') + "/" + $RelativeOrAbsoluteUri.OriginalString.TrimStart('/')) } function Convert-FederatedUser { [cmdletBinding()] param( [Array] $Users ) foreach ($FederatedUser in $Users) { $WorkEmail = $null $HomeEmail = $null $WorkPhone = $null $MobilePhone = $null $HomePhone = $null $Addresses = $FederatedUser.'addresses' foreach ($Address in $Addresses) { if ($Address.'type' -eq 'work') { $streetAddress = $Address.streetAddress $postalCode = $Address.PostalCode $city = $Address.Locality $region = $Address.region $country = $Address.country } elseif ($Address.'type' -eq 'home') { $HomeStreetAddress = $Address.streetAddress $HomePostalCode = $Address.PostalCode $HomeCity = $Address.Locality $HomeRegion = $Address.region $HomeCountry = $Address.country } } $Emails = $FederatedUser.'emails' foreach ($Email in $Emails) { if ($Email.Type -eq 'work') { $WorkEmail = $Email.Value } elseif ($Email.Type -eq 'home') { $HomeEmail = $Email.Value } } $PhoneNumbers = $FederatedUser.'phoneNumbers' foreach ($Phone in $PhoneNumbers) { if ($Phone.Type -eq 'work') { $WorkPhone = $Phone.Value } elseif ($Phone.Type -eq 'mobile') { $MobilePhone = $Phone.Value } elseif ($Phone.Type -eq 'home') { $HomePhone = $Phone.Value } } $CompanyLogoUrl = $null $CompanyThumbnailUrl = $null $CompanyLogos = $FederatedUser.'urn:ietf:params:scim:schemas:extension:fd:2.0:User:companyLogos' foreach ($C in $CompanyLogos) { if ($Type -eq 'logo') { $CompanyLogoUrl = $C.Value } elseif ($Type -eq 'thumbnail') { $CompanyThumbnailUrl = $C.Value } } $PhotoUrl = $null $ThumbnailUrl = $null $Photos = $FederatedUser.'photos' foreach ($C in $Photos) { if ($Type -eq 'photo') { $PhotoUrl = $C.Value } elseif ($Type -eq 'thumbnail') { $ThumbnailUrl = $C.Value } } [PSCustomObject] @{ Id = $FederatedUser.'id' ExternalId = $FederatedUser.'externalId' UserName = $FederatedUser.'userName' GivenName = $FederatedUser.'name'.'givenName' FamilyName = $FederatedUser.'name'.'familyName' DisplayName = $FederatedUser.'displayName' NickName = $FederatedUser.'nickName' ProfileUrl = $FederatedUser.'profileUrl' Title = $FederatedUser.'title' UserType = $FederatedUser.'userType' EmailAddress = $WorkEmail EmailAddressHome = $HomeEmail PhoneNumberWork = $WorkPhone PhoneNumberMobile = $MobilePhone PhoneNumberHome = $HomePhone StreetAddress = $streetAddress City = $City Region = $region PostalCode = $postalCode Country = $country StreetAddressHome = $HomeStreetAddress CityHome = $HomeCity RegionHome = $HomeRegion PostalCodeHome = $HomePostalCode CountryHome = $HomeCountry PreferredLanguage = $FederatedUser.'preferredLanguage' Locale = $FederatedUser.'locale' TimeZone = $FederatedUser.'timezone' Active = $FederatedUser.'active' Groups = $FederatedUser.'groups' Roles = $FederatedUser.'roles'.value Organization = $FederatedUser.'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'.'organization' EmployeeNumber = $FederatedUser.'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'.'employeeNumber' CostCenter = $FederatedUser.'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'.'costCenter' Division = $FederatedUser.'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'.'division' Department = $FederatedUser.'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'.'department' Manager = $FederatedUser.'urn:ietf:params:scim:schemas:extension:enterprise:2.0User'.'manager' Description = $FederatedUser.'urn:ietf:params:scim:schemas:extension:fd:2.0:User'.'description' DirectoryId = $FederatedUser.'urn:ietf:params:scim:schemas:extension:fd:2.0:User'.'directoryId' CompanyId = $FederatedUser.'urn:ietf:params:scim:schemas:extension:fd:2.0:User'.'companyId' CompanyLogoUrl = $CompanyLogoUrl CompanyThumbnailUrl = $CompanyThumbnailUrl PhotoUrl = $PhotoUrl ThumbnailUrl = $ThumbnailUrl Password = $FederatedUser.Password ManagerID = $FederatedUser.'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'.'manager'.'value' #ManagerUserName = $FederatedUser.'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager'.'displayName' #ManagerReference = $FederatedUser.'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'.'manager'.'$ref' ManagerDisplayName = $FederatedUser.'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'.'manager'.'displayName' Role = $FederatedUser.'roles'.value Custom01 = $FederatedUser.'urn:ietf:params:scim:schemas:extension:fd:2.0:User'.'custom01' Custom02 = $FederatedUser.'urn:ietf:params:scim:schemas:extension:fd:2.0:User'.'custom02' Custom03 = $FederatedUser.'urn:ietf:params:scim:schemas:extension:fd:2.0:User'.'custom03' ResourceType = $FederatedUser.Meta.ResourceType Created = $FederatedUser.Meta.Created LastModified = $FederatedUser.Meta.LastModified Location = $FederatedUser.Meta.location } } } function Add-FederatedDirectoryUser { [alias('Add-FDUser')] [CmdletBinding(SupportsShouldProcess)] param( [System.Collections.IDictionary] $Authorization, [string] $ExternalId, [parameter()][string] $DirectoryID, [parameter(Mandatory)][string] $UserName, [Alias('FirstName')] $FamilyName, [string] $GivenName, [parameter(Mandatory)][string] $DisplayName, [string] $NickName, [string] $ProfileUrl, [string] $EmailAddress, [string] $EmailAddressHome, [string] $StreetAddress, [string] $City, [string] $Region, [string] $PostalCode, [string] $Country, [string] $StreetAddressHome, [string] $PostalCodeHome, [string] $CityHome, [string] $RegionHome, [string] $CountryHome, [string] $PhoneNumberWork, [string] $PhoneNumberHome, [string] $PhoneNumberMobile, [string] $PhotoUrl, [string] $ThumbnailUrl, [string] $CompanyID, # [string] $CompanyLogoUrl, # [string] $CompanyThumbnailUrl, [string] $PreferredLanguage, [string] $Locale, [string] $TimeZone, [string] $Title, [string] $UserType, [string] $Password, [string] $ManagerID, [string] $ManagerUserName, [string] $ManagerDisplayName, [switch] $Active, [string] $Department, [string] $EmployeeNumber, [string] $CostCenter, [string] $Division, [string] $Description, [ValidateSet('admin', 'user')][string] $Role = 'user', [alias('CustomAttribute01')][string] $Custom01, [alias('CustomAttribute02')][string] $Custom02, [alias('CustomAttribute03')][string] $Custom03, [switch] $Suppress, [switch] $BulkProcessing ) if (-not $Authorization) { if ($Script:AuthorizationCacheFD) { $Authorization = $Script:AuthorizationCacheFD[0] } if (-not $Authorization) { if ($PSBoundParameters.ErrorAction -eq 'Stop') { throw "No authorization found. Please run 'Connect-FederatedDirectory' first." } else { Write-Warning -Message "Add-FederatedDirectoryUser - No authorization found. Please run 'Connect-FederatedDirectory' first." return } } } if ($Authorization) { if ($ManagerUserName) { $ManagerID = (Get-FederatedDirectoryUser -Authorization $Authorization -UserName $ManagerUserName).Id } $Body = [ordered] @{ schemas = @( "urn:ietf:params:scim:schemas:core:2.0:User" "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" "urn:ietf:params:scim:schemas:extension:fd:2.0:User" ) "externalId" = $ExternalId "userName" = $UserName "name" = [ordered] @{ "familyName" = $FamilyName "givenName" = $GivenName } "displayName" = $DisplayName "nickName" = $NickName "profileUrl" = $ProfileUrl "emails" = @( if ($EmailAddress) { @{ "value" = $EmailAddress "type" = "work" "primary" = $true } } if ($EmailAddressHome) { @{ "value" = $EmailAddressHome "type" = "home" } } ) "addresses" = @( if ($StreetAddress -or $City -or $Region -or $PostalCode -or $Country) { @{ "streetAddress" = $StreetAddress "locality" = $City "region" = $Region "postalCode" = $PostalCode "country" = $Country "type" = "work" "primary" = $true } } if ($StreetAddressHome -or $CityHome -or $RegionHome -or $PostalCodeHome -or $CountryHome) { @{ "streetAddress" = $StreetAddressHome "locality" = $CityHome "region" = $RegionHome "postalCode" = $PostalCodeHome "country" = $CountryHome "type" = "home" } } ) "phoneNumbers" = @( if ($PhoneNumberWork) { @{ "value" = $PhoneNumberWork "type" = "work" "primary" = $true } } if ($PhoneNumberHome) { @{ "value" = $PhoneNumberHome "type" = "home" } } if ($PhoneNumberMobile) { @{ "value" = $PhoneNumberMobile "type" = "mobile" } } ) "photos" = @( if ($PhotoUrl) { @{ "value" = $PhotoUrl "type" = "photo" } } if ($ThumbnailUrl) { @{ "value" = $ThumbnailUrl "type" = "thumbnail" } } ) "password" = $Password "preferredLanguage" = $PreferredLanguage "locale" = $Locale "timeZone" = $TimeZone "userType" = $UserType "title" = $Title "active" = if ($PSBoundParameters.Keys -contains ('Active')) { $Active.IsPresent } else { $Null } "roles" = @( if ($Role) { @{ "value" = $Role "display" = $Role } } ) "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" = [ordered] @{ #"organization" = $Organization # read only... "department" = $Department "employeeNumber" = $EmployeeNumber "costCenter" = $CostCenter "division" = $Division "manager" = @{ "displayName" = $ManagerDisplayName "value" = $ManagerID #"`$ref" = $ManagerReference # readonly } } "urn:ietf:params:scim:schemas:extension:fd:2.0:User" = [ordered] @{ "description" = $Description "companyId" = $CompanyId # "companyLogos" = @( # if ($CompanyLogoUrl) { # @{ # "value" = $CompanyLogoUrl # "type" = "logo" # } # } # if ($CompanyThumbnailUrl) { # @{ # "value" = $CompanyThumbnailUrl # "type" = "thumbnail" # } # } # ) #'directoryId' = $DirectoryID 'custom01' = $Custom01 'custom02' = $Custom02 'custom03' = $Custom03 } } Try { Remove-EmptyValue -Hashtable $Body -Recursive -Rerun 2 # for troubleshooting if ($VerbosePreference -eq 'Continue') { $Body | ConvertTo-Json -Depth 10 | Write-Verbose } if ($BulkProcessing) { # Return body is used for using Invoke-FederatedDirectory to add/set/remove users in bulk $ReturnObject = [ordered] @{ data = $Body method = 'POST' bulkId = $Body.userName } # for troubleshooting if ($VerbosePreference -eq 'Continue') { $ReturnObject | ConvertTo-Json -Depth 10 | Write-Verbose } return $ReturnObject } $invokeRestMethodSplat = [ordered] @{ Method = 'POST' Uri = 'https://api.federated.directory/v2/Users' Headers = [ordered] @{ 'Content-Type' = 'application/json; charset=utf-8' 'Authorization' = $Authorization.Authorization 'Cache-Control' = 'no-cache' } Body = $Body | ConvertTo-Json -Depth 10 ErrorAction = 'Stop' ContentType = 'application/json; charset=utf-8' } if ($DirectoryID) { $invokeRestMethodSplat['Headers']['directoryId'] = $DirectoryID } if ($PSCmdlet.ShouldProcess("username $UserName, displayname $DisplayName", "Adding user")) { $ReturnData = Invoke-RestMethod @invokeRestMethodSplat -Verbose:$false # don't return data as we trust it's been created if (-not $Suppress) { $ReturnData } } # # for troubleshooting # if ($VerbosePreference -eq 'Continue') { # $invokeRestMethodSplat.Remove('body') # $invokeRestMethodSplat | ConvertTo-Json -Depth 10 | Write-Verbose # } } catch { if ($PSBoundParameters.ErrorAction -eq 'Stop') { throw } else { $ErrorDetails = $_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue if ($ErrorDetails.Detail -like '*already exists*directory*') { Write-Warning -Message "Add-FederatedDirectoryUser - $($ErrorDetails.Detail) [UserName: $UserName / DisplayName: $DisplayName]" } else { Write-Warning -Message "Add-FederatedDirectoryUser - Error $($_.Exception.Message), $($ErrorDetails.Detail)" } } } } else { Write-Warning -Message 'Add-FederatedDirectoryUser - No authorization found. Please make sure to use Connect-FederatedDirectory first.' } } function Connect-FederatedDirectory { <# .SYNOPSIS Connects to a federated directory. .DESCRIPTION Connects to a federated directory. .PARAMETER Token The token to use for authentication to the federated directory from New-JWT command. This is the default. .PARAMETER TokenEncrypted The encrypted token to use for authentication to the federated directory from New-JWT command. .PARAMETER ExpiresTimeout The number of seconds before the token expires. .PARAMETER ForceRefresh Forces a refresh of the authentication .PARAMETER Suppress Suppresses the output of the command. By default the command will output the connection information. .EXAMPLE $Token = 'TokenInformation' Connect-FederatedDirectory -Token $Token -Suppress .NOTES General notes #> [alias('Connect-FD')] [cmdletbinding(DefaultParameterSetName = 'ClearText')] param( [Parameter(Mandatory, ParameterSetName = 'ClearText')] [alias('ApplicationSecret', 'ApplicationKey')] [string] $Token, [Parameter(Mandatory, ParameterSetName = 'Encrypted')] [alias('ApplicationSecretEncrypted', 'ApplicationKeyEncrypted')] [string] $TokenEncrypted, [int] $ExpiresTimeout = 30, [switch] $ForceRefresh, [switch] $Suppress ) if (-not $Script:AuthorizationCacheFD) { $Script:AuthorizationCacheFD = [ordered] @{} } if ($TokenEncrypted) { try { $ApplicationKeyTemp = $TokenEncrypted | ConvertTo-SecureString -ErrorAction Stop } catch { if ($PSBoundParameters.ErrorAction -eq 'Stop') { throw } else { $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " " Write-Warning -Message "Connect-FederatedDirectory - Error: $ErrorMessage" return } } $ApplicationKey = [System.Net.NetworkCredential]::new([string]::Empty, $ApplicationKeyTemp).Password } else { $ApplicationKey = $Token } $ShortKey = $ApplicationKey.Trim(15) $RestSplat = @{ ErrorAction = 'Stop' Method = 'POST' Body = @{ "grant_type" = "urn:ietf:params:oauth:grant-type:jwt-bearer" "assertion" = $ApplicationKey } Uri = 'https://api.federated.directory/v2/Login/Oauth2/Token' } if ($Script:AuthorizationCacheFD[$ShortKey] -and -not $ForceRefesh) { if ($Script:AuthorizationCacheFD[$ShortKey].ExpiresOn -gt [datetime]::UtcNow) { Write-Verbose "Connect-FederatedDirectory - Using cache for $ShortKey..." if (-not $Suppress) { return $Script:AuthorizationCacheFD[$ShortKey] } } } try { $Authorization = Invoke-RestMethod @RestSplat $Key = [ordered] @{ 'Authorization' = "$($Authorization.token_type) $($Authorization.access_token)" 'Extended' = $Authorization 'Error' = '' 'ExpiresOn' = ([datetime]::UtcNow).AddSeconds($Authorization.expires_in - $ExpiresTimeout) 'Splat' = [ordered] @{ Token = $RestSplat['Body']['assertion'] } 'Platform' = $Platform } $Script:AuthorizationCacheFD[$ShortKey] = $Key } catch { if ($PSBoundParameters.ErrorAction -eq 'Stop') { throw } else { $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " " Write-Warning -Message "Connect-FederatedDirectory - Error: $ErrorMessage" $Key = [ordered] @{ 'Authorization' = $Null 'Extended' = $Null 'Error' = $ErrorMessage 'ExpiresOn' = $null 'Splat' = [ordered] @{ Token = $RestSplat['Body']['assertion'] } 'Platform' = $Platform } } } if (-not $Suppress) { $Key } } function Get-FederatedDirectorySchema { <# .SYNOPSIS Get the schema of a federated directory. .DESCRIPTION Get the schema of a federated directory. .EXAMPLE $Schema = Get-FederatedDirectorySchema $Schema | Where-Object { $_.Name -eq 'User' } | Select-Object -ExpandProperty Attributes | Format-Table $Schema | Where-Object { $_.Name -eq 'EnterpriseUser' } | Select-Object -ExpandProperty Attributes | Format-Table $Schema | Where-Object { $_.Name -eq 'FederatedDirectoryUser' } | Select-Object -ExpandProperty Attributes | Format-Table .NOTES General notes #> [alias('Get-FDSchema')] [cmdletbinding()] param() $BaseUri = "https://api.federated.directory/v2/Schemas" Write-Verbose -Message "Get-FederatedDirectorySchema - Using query: $BaseUri" $Headers = @{ 'Content-Type' = 'application/json' } Try { $BatchObjects = Invoke-RestMethod -Method Get -Uri $BaseUri -Headers $Headers -ErrorAction Stop $BatchObjects.Resources } catch { if ($PSBoundParameters.ErrorAction -eq 'Stop') { throw } else { $ErrorDetails = $_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue Write-Warning -Message "Get-FederatedDirectorySchema - Error $($_.Exception.Message), $($ErrorDetails.Detail)" } } } function Get-FederatedDirectoryUser { [alias('Get-FDUser')] [cmdletbinding()] param( [System.Collections.IDictionary] $Authorization, [string] $Id, [Alias('UserName')][string] $SearchUserName, [Alias('ExternalID')][string] $SearchExternalID, [string] $Search, [ValidateSet( 'id', 'externalId', 'userName', 'givenName', 'familyName', 'displayName', 'nickName', 'profileUrl', 'title', 'userType', 'emails', 'phoneNumbers', 'addresses', 'preferredLanguage', 'locale', 'timezone', 'active', 'groups', 'roles', 'meta', 'organization', 'employeeNumber', 'costCenter', 'division', 'department', 'manager', 'description', 'directoryId', 'companyId', 'companyLogos', 'custom01', 'custom02', 'custom03' )] [string] $SearchProperty = 'userName', [string] $SearchOperator = 'eq', [string] $DirectoryID, [int] $MaxResults, [int] $StartIndex = 1, [int] $Count = 1000, [string] $Filter, [ValidateSet( 'id', 'externalId', 'userName', 'givenName', 'familyName', 'displayName', 'nickName', 'profileUrl', 'title', 'userType', 'emails', 'phoneNumbers', 'addresses', 'preferredLanguage', 'locale', 'timezone', 'active', 'groups', 'roles', 'meta', 'organization', 'employeeNumber', 'costCenter', 'division', 'department', 'manager', 'description', 'directoryId', 'companyId', 'companyLogos', 'custom01', 'custom02', 'custom03' )] [string] $SortBy, [ValidateSet('ascending', 'descending')][string] $SortOrder, [Alias('Property')] [ValidateSet( 'id', 'externalId', 'userName', 'givenName', 'familyName', 'displayName', 'nickName', 'profileUrl', 'title', 'userType', 'emails', 'phoneNumbers', 'addresses', 'preferredLanguage', 'locale', 'timezone', 'active', 'groups', 'roles', 'meta', 'organization', 'employeeNumber', 'costCenter', 'division', 'department', 'manager', 'description', 'directoryId', 'companyId', 'companyLogos', 'custom01', 'custom02', 'custom03' )] [string[]] $Attributes, [switch] $Native ) $ConvertAttributes = @{ 'id' = 'id' 'externalId' = 'externalId' 'userName' = 'userName' 'givenName' = 'name.givenName' 'familyName' = 'name.familyName' 'displayName' = 'displayName' 'nickName' = 'nickName' 'profileUrl' = 'profileUrl' 'title' = 'title' 'userType' = 'userType' 'emails' = 'emails' 'phoneNumbers' = 'phoneNumbers' 'addresses' = 'addresses' 'preferredLanguage' = 'preferredLanguage' 'locale' = 'locale' 'timezone' = 'timezone' 'active' = 'active' 'groups' = 'groups' 'roles' = 'roles' 'meta' = 'meta' 'organization' = 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:organization' 'employeeNumber' = 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:employeeNumber' 'costCenter' = 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:costCenter' 'division' = 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:division' 'department' = 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department' 'manager' = 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager' 'description' = 'urn:ietf:params:scim:schemas:extension:fd:2.0:User:description' 'directoryId' = 'urn:ietf:params:scim:schemas:extension:fd:2.0:User:directoryId' 'companyId' = 'urn:ietf:params:scim:schemas:extension:fd:2.0:User:companyId' 'companyLogos' = 'urn:ietf:params:scim:schemas:extension:fd:2.0:User:companyLogos' 'custom01' = 'urn:ietf:params:scim:schemas:extension:fd:2.0:User:custom01' 'custom02' = 'urn:ietf:params:scim:schemas:extension:fd:2.0:User:custom02' 'custom03' = 'urn:ietf:params:scim:schemas:extension:fd:2.0:User:custom03' } $AttributesConverted = foreach ($Attribute in $Attributes) { if ($ConvertAttributes[$Attribute]) { $ConvertAttributes[$Attribute] } } if ($SortBy) { $SortByConverted = $ConvertAttributes[$SortBy] } if (-not $Authorization) { if ($Script:AuthorizationCacheFD) { $Authorization = $Script:AuthorizationCacheFD[0] } if (-not $Authorization) { if ($PSBoundParameters.ErrorAction -eq 'Stop') { throw "No authorization found. Please run 'Connect-FederatedDirectory' first." } else { Write-Warning -Message "Get-FederatedDirectory - No authorization found. Please run 'Connect-FederatedDirectory' first." return } } } if ($Authorization) { if ($ID) { $BaseUri = "https://api.federated.directory/v2/Users/$ID" } else { $BaseUri = "https://api.federated.directory/v2/Users" } # Lets build up query $QueryParameter = [ordered] @{ count = if ($Count) { $Count } else { $null } startIndex = if ($StartIndex) { $StartIndex } else { $null } filter = if ($SearchUserName) { # keep in mind regardless of used operator it will always revert back to co as per API (weird) "userName eq `"$SearchUserName`"" } elseif ($SearchExternalID) { "externalId eq `"$SearchExternalID`"" } elseif ($Search -and $SearchProperty) { "$($ConvertAttributes[$SearchProperty]) $SearchOperator `"$Search`"" } else { $Filter } sortBy = $SortByConverted sortOrder = $SortOrder attributes = $AttributesConverted -join "," } # lets remove empty values to remove whatever user hasn't requested Remove-EmptyValue -Hashtable $QueryParameter # Lets build our url $Uri = Join-UriQuery -BaseUri $BaseUri -QueryParameter $QueryParameter Write-Verbose -Message "Get-FederatedDirectoryUser - Using query: $Uri" $Headers = @{ 'Content-Type' = 'application/json; charset=utf-8' 'Authorization' = $Authorization.Authorization 'directoryID' = $DirectoryID } Remove-EmptyValue -Hashtable $Headers Try { $BatchObjects = Invoke-RestMethod -Method Get -Uri $Uri -Headers $Headers -ErrorAction Stop -ContentType 'application/json; charset=utf-8' } catch { if ($PSBoundParameters.ErrorAction -eq 'Stop') { throw } else { $ErrorDetails = $_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue if ($ErrorDetails.Detail -like '*already exists*directory*') { Write-Warning -Message "Get-FederatedDirectoryUser - $($ErrorDetails.Detail) [UserName: $UserName / ID: $ID]" return } else { Write-Warning -Message "Get-FederatedDirectoryUser - Error $($_.Exception.Message), $($ErrorDetails.Detail)" return } } } if ($BatchObjects.Resources) { Write-Verbose -Message "Get-FederatedDirectoryUser - Got $($BatchObjects.Resources.Count) users (StartIndex: $StartIndex, Count: $Count). Starting to process them." if ($MaxResults -gt 0 -and $BatchObjects.Resources.Count -ge $MaxResults) { # return users if amount of users available is more than we wanted if ($Native) { $BatchObjects.Resources | Select-Object -First $MaxResults } else { Convert-FederatedUser -Users ($BatchObjects.Resources | Select-Object -First $MaxResults) } $LimitReached = $true } else { # return all users that were given in a batch if ($Native) { $BatchObjects.Resources } else { Convert-FederatedUser -Users $BatchObjects.Resources } } } elseif ($BatchObjects.Schemas -and $BatchObjects.id) { if ($Native) { $BatchObjects } else { Convert-FederatedUser -Users $BatchObjects } } else { Write-Verbose "Get-FederatedDirectoryUser - No users found" return } if (-not $Count -and -not $StartIndex) { # paging is disabled, we don't do anything } elseif (-not $LimitReached -and $BatchObjects.TotalResults -gt $BatchObjects.StartIndex + $Count) { # lets get more users because there's more to get and user wanted more $MaxResults = $MaxResults - $BatchObjects.Resources.Count Write-Verbose "Get-FederatedDirectoryUser - Processing more pages (StartIndex: $StartIndex, Count: $Count)." $getFederatedDirectoryUserSplat = @{ Authorization = $Authorization StartIndex = $($BatchObjects.StartIndex + $Count) Count = $Count MaxResults = $MaxResults Filter = $Filter SortBy = $SortBy SortOrder = $SortOrder Attributes = $Attributes DirectoryID = $DirectoryID Native = $Native } Remove-EmptyValue -Hashtable $getFederatedDirectoryUserSplat Get-FederatedDirectoryUser @getFederatedDirectoryUserSplat } } else { Write-Warning -Message 'Get-FederatedDirectoryUser - No authorization found. Please make sure to use Connect-FederatedDirectory first.' } } function Invoke-FederatedDirectory { <# .SYNOPSIS Provides a way to invoke multiple operations on FederatedDirectory in a single request (bulk). .DESCRIPTION Provides a way to invoke multiple operations on FederatedDirectory in a single request (bulk). While the official limit is 1000 operations in a single request, it's actually much lower due to payload size .PARAMETER Authorization The authorization identity to use for the request from Connect-FederatedDirectory. If not specified, the default authorization identity will be used. .PARAMETER Operations Operations to perform as part of bulk request .PARAMETER Size Batch size of operations to send in a single request. Default is 100. .PARAMETER ReturnHashtable Return results as a hashtable for quick matching BulkId .PARAMETER ReturnNative Return results the same way REST API returns it PARAMETER Suppress Prevent returning results .EXAMPLE Connect-FederatedDirectory -Token $Token -Suppress $Operations = for ($i = 1; $i -le 1; $i++) { Add-FederatedDirectoryUser -UserName "TestNewwwww$i@test.pl" -DisplayName "TestUserNew$i" -ManagerDisplayName 'TestUser' -FamilyName 'Kłys' -GivenName 'Przemysłąw' -BulkProcessing #Set-FederatedDirectoryUser -Id '69c6b3c0-34dd-11ed-a621-4b6b819dffa2' -DisplayName 'New name' -FamilyName 'New namme' -EmailAddressHome 'test@evo.pl' -PhoneNumberHome '502469000' -Custom01 'test123' -Action Update -BulkProcessing Set-FederatedDirectoryUser -Id '0c50c6f0-3428-11ed-98e2-11027423d1f1' -DisplayName 'New name' -GivenName "Test" -EmailAddressHome 'test@evo.pl' -PhoneNumberHome '502469000' -Custom01 'test123' -UserName 'TestMe@verymuch.pl' -Action Overwrite -StreetAddress "Test me" -BulkProcessing Set-FederatedDirectoryUser -Id '69c6b3c0-34dd-11ed-a621-4b6b819dffa2' -DisplayName 'New name' -GivenName "Test" -EmailAddressHome 'test@evo.pl' -PhoneNumberHome '502469000' -Custom01 'test123' -UserName 'TestMe@verymuch.pl' -Action Overwrite -StreetAddress "Test me" -BulkProcessing } $Response = Invoke-FederatedDirectory -Operations $Operations -ReturnHashtable $Response | Format-Table .NOTES General notes #> [cmdletbinding(SupportsShouldProcess)] param( [System.Collections.IDictionary] $Authorization, [Array] $Operations, [int] $Size = 1000, [switch] $ReturnHashtable, [switch] $ReturnNative, [switch] $Suppress ) $TranslateMethod = @{ 'PUT' = 'Update' 'POST' = 'Add' 'DELETE' = 'Remove' } $TranslateStatus = @{ '200' = $true '201' = $true '204' = $true '400' = $false '401' = $false '403' = $false '404' = $false '409' = $false '500' = $false '503' = $false } if (-not $Authorization) { if ($Script:AuthorizationCacheFD) { $Authorization = $Script:AuthorizationCacheFD[0] } if (-not $Authorization) { if ($PSBoundParameters.ErrorAction -eq 'Stop') { throw "No authorization found. Please run 'Connect-FederatedDirectory' first." } else { Write-Warning -Message "Invoke-FederatedDirectory - No authorization found. Please run 'Connect-FederatedDirectory' first." return } } } if ($Authorization) { $SplitOperations = Split-Array -Objects $Operations -Size $Size foreach ($O in $SplitOperations) { $Body = [ordered] @{ schemas = @('urn:ietf:params:scim:api:messages:2.0:BulkRequest') Operations = @($O) } Remove-EmptyValue -Hashtable $Body -Recursive -Rerun 2 $invokeRestMethodSplat = [ordered] @{ Method = 'POST' Uri = 'https://api.federated.directory/v2/Bulk' Headers = [ordered] @{ 'Content-Type' = 'application/json; charset=utf-8' 'Authorization' = $Authorization.Authorization 'Cache-Control' = 'no-cache' } Body = $Body | ConvertTo-Json -Depth 10 ErrorAction = 'Stop' ContentType = 'application/json; charset=utf-8' } if ($DirectoryID) { $invokeRestMethodSplat['Headers']['directoryId'] = $DirectoryID } # for troubleshooting if ($VerbosePreference -eq 'Continue') { $Body | ConvertTo-Json -Depth 10 | Write-Verbose } $Count = ($O | Measure-Object).Count Try { if ($PSCmdlet.ShouldProcess("Federated Directory", "Bulk sending $($Count) operations")) { $ReturnData = Invoke-RestMethod @invokeRestMethodSplat -Verbose:$false # don't return data as we trust it's been created } } catch { if ($PSBoundParameters.ErrorAction -eq 'Stop') { throw } else { $ErrorDetails = $_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue Write-Warning -Message "Invoke-FederatedDirectory - Error processing $($_.Exception.Message), $($ErrorDetails.Detail)" } } if ($ReturnData -and -not $Suppress) { if ($ReturnNative) { $ReturnData.Operations } elseif ($ReturnHashtable) { $ResultsPrepared = [ordered] @{} foreach ($Operation in $ReturnData.Operations) { if ($Operation.method -and $Operation.status) { $ResultsPrepared[$Operation.bulkid] = [PSCustomObject] @{ BulkID = $Operation.bulkid Method = $TranslateMethod[$Operation.method] Status = $TranslateStatus[$Operation.status.code.ToString()] StatusResponse = $Operation.status.code Detail = $Operation.response.detail ScimType = $Operation.response.scimType Location = $Operation.location } } else { Write-Warning -Message "Invoke-FederatedDirectory - Error processing, wrong return code. Error: $($Operation.details.message)" } } $ResultsPrepared } else { foreach ($Operation in $ReturnData.Operations) { if ($Operation.method -and $Operation.status) { [PSCustomObject] @{ BulkID = $Operation.bulkid Method = $TranslateMethod[$Operation.method] Status = $TranslateStatus[$Operation.status.code.ToString()] StatusResponse = $Operation.status.code Detail = $Operation.response.detail ScimType = $Operation.response.scimType Location = $Operation.location } } else { Write-Warning -Message "Invoke-FederatedDirectory - Error processing, wrong return code. Error: $($Operation.details.message)" } } } } # # for troubleshooting # if ($VerbosePreference -eq 'Continue') { # $invokeRestMethodSplat.Remove('body') # $invokeRestMethodSplat | ConvertTo-Json -Depth 10 | Write-Verbose # } } } else { Write-Warning -Message 'Invoke-FederatedDirectory - No authorization found. Please make sure to use Connect-FederatedDirectory first.' } } function Remove-FederatedDirectoryUser { <# .SYNOPSIS Remove a user from a federated directory. .DESCRIPTION Remove a user from a federated directory. .PARAMETER Authorization The authorization identity to use for the request from Connect-FederatedDirectory. If not specified, the default authorization identity will be used. .PARAMETER User The user to remove from the federated directory. .PARAMETER Id The id of the user to remove from the federated directory. .PARAMETER SearchUserName The user name of the user to remove from the federated directory. .PARAMETER DirectoryID The id of the directory to remove the user from. If not specified, the default directory will be used. .PARAMETER All Remove all users from the directory. .EXAMPLE # remove specific user id Remove-FederatedDirectoryUser -Id '171a8cd0-2382-11ed-9dd1-b13400d703b6' -Verbose .EXAMPLE # get all ther users that contain name test user and delete them Remove-FederatedDirectoryUser -UserName 'testuser' -Verbose .EXAMPLE # get all ther users that contain name test user and delete them Get-FederatedDirectoryUser -UserName 'testuser' | Remove-FederatedDirectoryUser -Verbose .NOTES General notes #> [alias('Remove-FDUser')] [CmdletBinding(DefaultParameterSetName = 'Id', SupportsShouldProcess)] param( [System.Collections.IDictionary] $Authorization, [parameter(Position = 0, ValueFromPipeline, Mandatory, ParameterSetName = 'User')][PSCustomObject[]] $User, [parameter(Mandatory, ParameterSetName = 'Id')][string[]] $Id, [parameter(Mandatory, ParameterSetName = 'UserName')][string[]] $SearchUserName, [parameter()][string] $DirectoryID, [switch] $BulkProcessing, [switch] $Suppress, [parameter(ParameterSetName = 'All')][switch] $All ) Begin { if (-not $Authorization) { if ($Script:AuthorizationCacheFD) { $Authorization = $Script:AuthorizationCacheFD[0] } if (-not $Authorization) { if ($PSBoundParameters.ErrorAction -eq 'Stop') { throw "No authorization found. Please run 'Connect-FederatedDirectory' first." } else { Write-Warning -Message "Remove-FederatedDirectoryUser - No authorization found. Please run 'Connect-FederatedDirectory' first." return } } } } Process { if ($Authorization) { if ($All) { # lets simplify this for all $Users = Get-FederatedDirectoryUser -Authorization $Authorization -DirectoryID $DirectoryID $Remove = foreach ($U in $Users) { Remove-FederatedDirectoryUser -Authorization $Authorization -Id $U.Id -BulkProcessing -DirectoryID $DirectoryID } Invoke-FederatedDirectory -Authorization $Authorization -Operations $Remove -Suppress:$Suppress.IsPresent } else { if ($Id) { $RemoveID = $Id } elseif ($User) { $RemoveID = $User.Id } elseif ($SearchUserName) { $RemoveID = Foreach ($U in $SearchUserName) { (Get-FederatedDirectoryUser -Authorization $Authorization -UserName $U).Id } } else { return } foreach ($I in $RemoveID) { Try { if ($BulkProcessing) { # Return body is used for using Invoke-FederatedDirectory to add/set/remove users in bulk return [ordered] @{ data = @{ schemas = @("urn:ietf:params:scim:schemas:core:2.0:User") id = $I } method = 'DELETE' bulkId = $I } } $invokeRestMethodSplat = [ordered] @{ Method = 'DELETE' Uri = "https://api.federated.directory/v2/Users/$I" Headers = [ordered] @{ 'Content-Type' = 'application/json' 'Authorization' = $Authorization.Authorization 'Cache-Control' = 'no-cache' 'directoryId' = $DirectoryID } ErrorAction = 'Stop' ContentType = 'application/json; charset=utf-8' } Remove-EmptyValue -Hashtable $invokeRestMethodSplat -Recursive if ($VerbosePreference -eq 'Continue') { $invokeRestMethodSplat | ConvertTo-Json -Depth 10 | Write-Verbose } if ($PSCmdlet.ShouldProcess($I, "Removing user")) { $ReturnData = Invoke-RestMethod @invokeRestMethodSplat if (-not $Suppress) { $ReturnData } } } catch { $ErrorDetails = $_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue if ($ErrorDetails.Detail -like "*not found*") { Write-Warning -Message "Remove-FederatedDirectoryUser - $($ErrorDetails.Detail)." } else { Write-Warning -Message "Remove-FederatedDirectoryUser - Error $($_.Exception.Message), $($ErrorDetails.Detail)" } } } } } else { Write-Warning -Message 'Remove-FederatedDirectoryUser - No authorization found. Please make sure to use Connect-FederatedDirectory first.' } } } function Set-FederatedDirectoryUser { [alias('Set-FDUser')] [CmdletBinding(SupportsShouldProcess)] param( [System.Collections.IDictionary] $Authorization, [string] $SearchUserName, [string] $Id, [string] $ExternalId, [parameter()][string] $DirectoryID, [parameter()][string] $UserName, [Alias('FirstName')] $FamilyName, [string] $GivenName, [parameter()][string] $DisplayName, [string] $NickName, [string] $ProfileUrl, [string] $EmailAddress, [string] $EmailAddressHome, [string] $StreetAddress, [string] $City, [string] $Region, [string] $PostalCode, [string] $Country, [string] $StreetAddressHome, [string] $PostalCodeHome, [string] $CityHome, [string] $RegionHome, [string] $CountryHome, [string] $PhoneNumberWork, [string] $PhoneNumberHome, [string] $PhoneNumberMobile, [string] $PhotoUrl, [string] $ThumbnailUrl, [string] $CompanyID, #[string] $CompanyLogoUrl, #[string] $CompanyThumbnailUrl, [string] $PreferredLanguage, [string] $Locale, [string] $TimeZone, [string] $Title, [string] $UserType, [string] $Password, [string] $ManagerID, [string] $ManagerUserName, [string] $ManagerReference, [string] $ManagerDisplayName, [bool] $Active, #[string] $Organization, [string] $Department, [string] $EmployeeNumber, [string] $CostCenter, [string] $Division, [string] $Description, [ValidateSet('admin', 'user', 'contact')][string] $Role, [alias('CustomAttribute01')][string] $Custom01, [alias('CustomAttribute02')][string] $Custom02, [alias('CustomAttribute03')][string] $Custom03, [switch] $Suppress, [ValidateSet('Overwrite', 'Update')][string] $Action = 'Update', [System.Collections.IDictionary] $ActionPerProperty = @{}, [switch] $BulkProcessing ) if (-not $Authorization) { if ($Script:AuthorizationCacheFD) { $Authorization = $Script:AuthorizationCacheFD[0] } if (-not $Authorization) { if ($PSBoundParameters.ErrorAction -eq 'Stop') { throw "No authorization found. Please run 'Connect-FederatedDirectory' first." } else { Write-Warning -Message "Set-FederatedDirectoryUser - No authorization found. Please run 'Connect-FederatedDirectory' first." return } } } if ($Authorization) { if ($Id) { $SetID = $Id } elseif ($User) { $SetID = $User.Id } elseif ($SearchUserName) { $SetID = Foreach ($U in $SearchUserName) { (Get-FederatedDirectoryUser -Authorization $Authorization -UserName $U).Id } } else { Write-Warning -Message "Set-FederatedDirectoryUser - No ID or UserName specified." return } if ($ManagerUserName) { $ManagerID = (Get-FederatedDirectoryUser -Authorization $Authorization -UserName $ManagerUserName).Id } if ($Action -eq 'Update') { $TranslatePath = @{ UserName = "userName" ExternalId = "externalId" FamilyName = "name.familyName" Password = "password" # not used yet # not used yet Role = "roles.value" # "admin" or "user" GivenName = 'name.givenName' DisplayName = 'displayName' NickName = 'nickName' ProfileUrl = 'profileUrl' EmailAddress = 'emails[type eq "work"].value' EmailAddressHome = 'emails[type eq "home"].value' StreetAddress = 'addresses[type eq "work"].streetAddress' City = 'addresses[type eq "work"].locality' Region = 'addresses[type eq "work"].region' PostalCode = 'addresses[type eq "work"].postalCode' Country = 'addresses[type eq "work"].country' StreetAddressHome = 'addresses[type eq "home"].streetAddress' PostalCodeHome = 'addresses[type eq "home"].postalCode' CityHome = 'addresses[type eq "home"].locality' RegionHome = 'addresses[type eq "home"].region' CountryHome = 'addresses[type eq "home"].country' PhoneNumberWork = 'phoneNumbers[type eq "work"].value' PhoneNumberHome = 'phoneNumbers[type eq "home"].value' PhoneNumberMobile = 'phoneNumbers[type eq "mobile"].value' PhotoUrl = 'photos[type eq "photo"].value' ThumbnailUrl = 'photos[type eq "thumbnail"].value' Title = 'title' UserType = 'userType' Active = 'active' TimeZone = 'timezone' Locale = 'locale' PreferredLanguage = 'preferredLanguage' EmployeeNumber = 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:employeeNumber' CostCenter = 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:costCenter' Division = 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:division' Department = 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department' Organization = 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:organization' ManagerID = 'manager' ManagerUserName = 'manager' ManagerDisplayName = 'manager' Description = "urn:ietf:params:scim:schemas:extension:fd:2.0:User:description" Custom01 = "urn:ietf:params:scim:schemas:extension:fd:2.0:User:custom01" Custom02 = "urn:ietf:params:scim:schemas:extension:fd:2.0:User:custom02" Custom03 = "urn:ietf:params:scim:schemas:extension:fd:2.0:User:custom03" CompanyID = "urn:ietf:params:scim:schemas:extension:fd:2.0:User:companyId" #CompanyLogoUrl = 'urn:ietf:params:scim:schemas:extension:fd:2.0:User:companyLogos[type eq "logo"].value' #CompanyThumbnailUrl = 'urn:ietf:params:scim:schemas:extension:fd:2.0:User:companyLogos[type eq "thumbnail"].value' } $Body = [ordered] @{ schemas = @( "urn:ietf:params:scim:api:messages:2.0:PatchOp" ) Operations = @( foreach ($Key in $PSBoundParameters.Keys) { if ($Key -in $TranslatePath.Keys) { if ($TranslatePath[$Key]) { $Path = $TranslatePath[$Key] } else { $Path = $Key } if ($null -ne $PSBoundParameters[$Key]) { if ($Key -eq 'ManagerUserName') { if ($ManagerID) { $Value = $ManagerID } else { $Value = $null } } elseif ($Key -eq 'ManagerDisplayName') { $Value = @{ displayName = $ManagerDisplayName } } else { $Value = $PSBoundParameters[$Key] } } else { $Value = $null } if ($ActionPerProperty) { if ($ActionPerProperty[$Key]) { $ActionProperty = $ActionPerProperty[$Key] } else { $ActionProperty = 'replace' } } else { $ActionProperty = 'replace' } if ($null -ne $Value) { [ordered] @{ op = $ActionProperty path = $Path value = $Value } } } } ) } } else { $Body = [ordered] @{ schemas = @( "urn:ietf:params:scim:schemas:core:2.0:User" "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" "urn:ietf:params:scim:schemas:extension:fd:2.0:User" ) # Mandatory "id" = $Id "externalId" = $ExternalId "userName" = $UserName "name" = [ordered] @{ "familyName" = $FamilyName "givenName" = $GivenName } "displayName" = $DisplayName "nickName" = $NickName "profileUrl" = $ProfileUrl "emails" = @( if ($EmailAddress) { [ordered]@{ "value" = $EmailAddress "type" = "work" "primary" = $true } } if ($EmailAddressHome) { [ordered]@{ "value" = $EmailAddressHome "type" = "home" } } ) "addresses" = @( if ($StreetAddress -or $City -or $Region -or $PostalCode -or $Country) { $StreetHash = [ordered]@{ "streetAddress" = $StreetAddress "locality" = $City "region" = $Region "postalCode" = $PostalCode "country" = $Country "type" = "work" "primary" = $true } Remove-EmptyValue -Hashtable $StreetHash if ($StreetHash) { $StreetHash } } if ($StreetAddressHome -or $CityHome -or $RegionHome -or $PostalCodeHome -or $CountryHome) { $StreetHash = [ordered]@{ "streetAddress" = $StreetAddressHome "locality" = $CityHome "region" = $RegionHome "postalCode" = $PostalCodeHome "country" = $CountryHome "type" = "home" } Remove-EmptyValue -Hashtable $StreetHash if ($StreetHash) { $StreetHash } } ) "phoneNumbers" = @( if ($PhoneNumberWork) { [ordered]@{ "value" = $PhoneNumberWork "type" = "work" "primary" = $true } } if ($PhoneNumberHome) { [ordered]@{ "value" = $PhoneNumberHome "type" = "home" } } if ($PhoneNumberMobile) { [ordered]@{ "value" = $PhoneNumberMobile "type" = "mobile" } } ) "photos" = @( if ($PhotoUrl) { [ordered]@{ "value" = $PhotoUrl "type" = "photo" } } if ($ThumbnailUrl) { [ordered]@{ "value" = $ThumbnailUrl "type" = "thumbnail" } } ) "password" = $Password "preferredLanguage" = $PreferredLanguage "locale" = $Locale "timeZone" = $TimeZone "userType" = $UserType "title" = $Title "active" = if ($PSBoundParameters.Keys -contains ('Active')) { $Active } else { $Null } "roles" = @( if ($Role) { @{ "value" = $Role "display" = $Role } } else { #@{ # "value" = 'user' # "display" = 'user' #} } ) "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" = [ordered] @{ #"organization" = $Organization # read only... "department" = $Department "employeeNumber" = $EmployeeNumber "costCenter" = $CostCenter "division" = $Division "manager" = @{ "displayName" = $ManagerDisplayName "value" = $ManagerID } } "urn:ietf:params:scim:schemas:extension:fd:2.0:User" = [ordered] @{ "description" = $Description "companyId" = $CompanyId # "companyLogos" = @( # if ($CompanyLogoUrl) { # @{ # "value" = $CompanyLogoUrl # "type" = "logo" # } # } # if ($CompanyThumbnailUrl) { # @{ # "value" = $CompanyThumbnailUrl # "type" = "thumbnail" # } # } # ) #'directoryId' = $DirectoryID 'custom01' = $Custom01 'custom02' = $Custom02 'custom03' = $Custom03 } } } Try { Remove-EmptyValue -Hashtable $Body -Recursive -Rerun 3 $MethodChosen = if ($Action -eq 'Update') { 'PATCH' } else { 'PUT' } if ($BulkProcessing) { # Return body is used for using Invoke-FederatedDirectory to add/set/remove users in bulk if ($Action -eq 'Update') { Write-Warning -Message "Bulk processing is not supported for Update action. Only Overwrite action is supported. Change action to Overwrite or don't use bulk processing for updates." } else { return [ordered] @{ data = $Body method = $MethodChosen bulkId = $SetID } } } $invokeRestMethodSplat = [ordered] @{ Method = $MethodChosen Uri = "https://api.federated.directory/v2/Users/$SetID" Headers = [ordered] @{ 'Content-Type' = 'application/json' 'Authorization' = $Authorization.Authorization 'Cache-Control' = 'no-cache' } Body = $Body | ConvertTo-Json -Depth 10 ErrorAction = 'Stop' ContentType = 'application/json; charset=utf-8' } if ($DirectoryID) { $invokeRestMethodSplat['Headers']['directoryId'] = $DirectoryID } # for troubleshooting if ($VerbosePreference -eq 'Continue') { $Body | ConvertTo-Json -Depth 10 | Write-Verbose } if ($PSCmdlet.ShouldProcess($SetID, "Updating user using $Action method")) { $ReturnData = Invoke-RestMethod @invokeRestMethodSplat # don't return data as we trust it's been updated if (-not $Suppress) { $ReturnData } } # # for troubleshooting # if ($VerbosePreference -eq 'Continue') { # $invokeRestMethodSplat.Remove('body') # $invokeRestMethodSplat | ConvertTo-Json -Depth 10 | Write-Verbose # } } catch { if ($PSBoundParameters.ErrorAction -eq 'Stop') { throw } else { $ErrorDetails = $_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue if ($ErrorDetails.Detail -like '*userName is mandatory*') { Write-Warning -Message "Set-FederatedDirectoryUser - $($ErrorDetails.Detail) [Id: $SetID]" } else { Write-Warning -Message "Set-FederatedDirectoryUser - Error $($_.Exception.Message), $($ErrorDetails.Detail) [Id: $SetID]" } } } } else { Write-Warning -Message 'Set-FederatedDirectoryUser - No authorization found. Please make sure to use Connect-FederatedDirectory first.' } } # Export functions and aliases as required Export-ModuleMember -Function @('Add-FederatedDirectoryUser', 'Connect-FederatedDirectory', 'Get-FederatedDirectorySchema', 'Get-FederatedDirectoryUser', 'Invoke-FederatedDirectory', 'Remove-FederatedDirectoryUser', 'Set-FederatedDirectoryUser') -Alias @('Add-FDUser', 'Connect-FD', 'Get-FDSchema', 'Get-FDUser', 'Remove-FDUser', 'Set-FDUser') # SIG # Begin signature block # MIInPgYJKoZIhvcNAQcCoIInLzCCJysCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAOMt32fStG40gv # dAOTUvDHYNZHw4Wl1r7wVi+1V14cG6CCITcwggO3MIICn6ADAgECAhAM5+DlF9hG # /o/lYPwb8DA5MA0GCSqGSIb3DQEBBQUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV # BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBa # Fw0zMTExMTAwMDAwMDBaMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy # dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0RpZ2lD # ZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC # AQoCggEBAK0OFc7kQ4BcsYfzt2D5cRKlrtwmlIiq9M71IDkoWGAM+IDaqRWVMmE8 # tbEohIqK3J8KDIMXeo+QrIrneVNcMYQq9g+YMjZ2zN7dPKii72r7IfJSYd+fINcf # 4rHZ/hhk0hJbX/lYGDW8R82hNvlrf9SwOD7BG8OMM9nYLxj+KA+zp4PWw25EwGE1 # lhb+WZyLdm3X8aJLDSv/C3LanmDQjpA1xnhVhyChz+VtCshJfDGYM2wi6YfQMlqi # uhOCEe05F52ZOnKh5vqk2dUXMXWuhX0irj8BRob2KHnIsdrkVxfEfhwOsLSSplaz # vbKX7aqn8LfFqD+VFtD/oZbrCF8Yd08CAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGG # MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEXroq/0ksuCMS1Ri6enIZ3zbcgP # MB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqGSIb3DQEBBQUA # A4IBAQCiDrzf4u3w43JzemSUv/dyZtgy5EJ1Yq6H6/LV2d5Ws5/MzhQouQ2XYFwS # TFjk0z2DSUVYlzVpGqhH6lbGeasS2GeBhN9/CTyU5rgmLCC9PbMoifdf/yLil4Qf # 6WXvh+DfwWdJs13rsgkq6ybteL59PyvztyY1bV+JAbZJW58BBZurPSXBzLZ/wvFv # hsb6ZGjrgS2U60K3+owe3WLxvlBnt2y98/Efaww2BxZ/N3ypW2168RJGYIPXJwS+ # S86XvsNnKmgR34DnDDNmvxMNFG7zfx9jEB76jRslbWyPpbdhAbHSoyahEHGdreLD # +cOZUbcrBwjOLuZQsqf6CkUvovDyMIIFMDCCBBigAwIBAgIQBAkYG1/Vu2Z1U0O1 # b5VQCDANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGln # aUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtE # aWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMTMxMDIyMTIwMDAwWhcNMjgx # MDIyMTIwMDAwWjByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5j # MRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBT # SEEyIEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMIIBIjANBgkqhkiG9w0BAQEF # AAOCAQ8AMIIBCgKCAQEA+NOzHH8OEa9ndwfTCzFJGc/Q+0WZsTrbRPV/5aid2zLX # cep2nQUut4/6kkPApfmJ1DcZ17aq8JyGpdglrA55KDp+6dFn08b7KSfH03sjlOSR # I5aQd4L5oYQjZhJUM1B0sSgmuyRpwsJS8hRniolF1C2ho+mILCCVrhxKhwjfDPXi # TWAYvqrEsq5wMWYzcT6scKKrzn/pfMuSoeU7MRzP6vIK5Fe7SrXpdOYr/mzLfnQ5 # Ng2Q7+S1TqSp6moKq4TzrGdOtcT3jNEgJSPrCGQ+UpbB8g8S9MWOD8Gi6CxR93O8 # vYWxYoNzQYIH5DiLanMg0A9kczyen6Yzqf0Z3yWT0QIDAQABo4IBzTCCAckwEgYD # VR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYB # BQUHAwMweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5k # aWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0 # LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwgYEGA1UdHwR6MHgwOqA4 # oDaGNGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJv # b3RDQS5jcmwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy # dEFzc3VyZWRJRFJvb3RDQS5jcmwwTwYDVR0gBEgwRjA4BgpghkgBhv1sAAIEMCow # KAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCgYIYIZI # AYb9bAMwHQYDVR0OBBYEFFrEuXsqCqOl6nEDwGD5LfZldQ5YMB8GA1UdIwQYMBaA # FEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqGSIb3DQEBCwUAA4IBAQA+7A1aJLPz # ItEVyCx8JSl2qB1dHC06GsTvMGHXfgtg/cM9D8Svi/3vKt8gVTew4fbRknUPUbRu # pY5a4l4kgU4QpO4/cY5jDhNLrddfRHnzNhQGivecRk5c/5CxGwcOkRX7uq+1UcKN # JK4kxscnKqEpKBo6cSgCPC6Ro8AlEeKcFEehemhor5unXCBc2XGxDI+7qPjFEmif # z0DLQESlE/DmZAwlCEIysjaKJAL+L3J+HNdJRZboWR3p+nRka7LrZkPas7CM1ekN # 3fYBIM6ZMWM9CBoYs4GbT8aTEAb8B4H6i9r5gkn3Ym6hU/oSlBiFLpKR6mhsRDKy # ZqHnGKSaZFHvMIIFPTCCBCWgAwIBAgIQBNXcH0jqydhSALrNmpsqpzANBgkqhkiG # 9w0BAQsFADByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkw # FwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEy # IEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMB4XDTIwMDYyNjAwMDAwMFoXDTIz # MDcwNzEyMDAwMFowejELMAkGA1UEBhMCUEwxEjAQBgNVBAgMCcWabMSFc2tpZTER # MA8GA1UEBxMIS2F0b3dpY2UxITAfBgNVBAoMGFByemVteXPFgmF3IEvFgnlzIEVW # T1RFQzEhMB8GA1UEAwwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMIIBIjANBgkq # hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv7KB3iyBrhkLUbbFe9qxhKKPBYqDBqln # r3AtpZplkiVjpi9dMZCchSeT5ODsShPuZCIxJp5I86uf8ibo3vi2S9F9AlfFjVye # 3dTz/9TmCuGH8JQt13ozf9niHecwKrstDVhVprgxi5v0XxY51c7zgMA2g1Ub+3ti # i0vi/OpmKXdL2keNqJ2neQ5cYly/GsI8CREUEq9SZijbdA8VrRF3SoDdsWGf3tZZ # zO6nWn3TLYKQ5/bw5U445u/V80QSoykszHRivTj+H4s8ABiforhi0i76beA6Ea41 # zcH4zJuAp48B4UhjgRDNuq8IzLWK4dlvqrqCBHKqsnrF6BmBrv+BXQIDAQABo4IB # xTCCAcEwHwYDVR0jBBgwFoAUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHQYDVR0OBBYE # FBixNSfoHFAgJk4JkDQLFLRNlJRmMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAK # BggrBgEFBQcDAzB3BgNVHR8EcDBuMDWgM6Axhi9odHRwOi8vY3JsMy5kaWdpY2Vy # dC5jb20vc2hhMi1hc3N1cmVkLWNzLWcxLmNybDA1oDOgMYYvaHR0cDovL2NybDQu # ZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwTAYDVR0gBEUwQzA3 # BglghkgBhv1sAwEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQu # Y29tL0NQUzAIBgZngQwBBAEwgYQGCCsGAQUFBwEBBHgwdjAkBggrBgEFBQcwAYYY # aHR0cDovL29jc3AuZGlnaWNlcnQuY29tME4GCCsGAQUFBzAChkJodHRwOi8vY2Fj # ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyQXNzdXJlZElEQ29kZVNpZ25p # bmdDQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAmr1sz4ls # LARi4wG1eg0B8fVJFowtect7SnJUrp6XRnUG0/GI1wXiLIeow1UPiI6uDMsRXPHU # F/+xjJw8SfIbwava2eXu7UoZKNh6dfgshcJmo0QNAJ5PIyy02/3fXjbUREHINrTC # vPVbPmV6kx4Kpd7KJrCo7ED18H/XTqWJHXa8va3MYLrbJetXpaEPpb6zk+l8Rj9y # G4jBVRhenUBUUj3CLaWDSBpOA/+sx8/XB9W9opYfYGb+1TmbCkhUg7TB3gD6o6ES # Jre+fcnZnPVAPESmstwsT17caZ0bn7zETKlNHbc1q+Em9kyBjaQRcEQoQQNpezQu # g9ufqExx6lHYDjCCBY0wggR1oAMCAQICEA6bGI750C3n79tQ4ghAGFowDQYJKoZI # hvcNAQEMBQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ # MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNz # dXJlZCBJRCBSb290IENBMB4XDTIyMDgwMTAwMDAwMFoXDTMxMTEwOTIzNTk1OVow # YjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ # d3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290 # IEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAv+aQc2jeu+RdSjww # IjBpM+zCpyUuySE98orYWcLhKac9WKt2ms2uexuEDcQwH/MbpDgW61bGl20dq7J5 # 8soR0uRf1gU8Ug9SH8aeFaV+vp+pVxZZVXKvaJNwwrK6dZlqczKU0RBEEC7fgvMH # hOZ0O21x4i0MG+4g1ckgHWMpLc7sXk7Ik/ghYZs06wXGXuxbGrzryc/NrDRAX7F6 # Zu53yEioZldXn1RYjgwrt0+nMNlW7sp7XeOtyU9e5TXnMcvak17cjo+A2raRmECQ # ecN4x7axxLVqGDgDEI3Y1DekLgV9iPWCPhCRcKtVgkEy19sEcypukQF8IUzUvK4b # A3VdeGbZOjFEmjNAvwjXWkmkwuapoGfdpCe8oU85tRFYF/ckXEaPZPfBaYh2mHY9 # WV1CdoeJl2l6SPDgohIbZpp0yt5LHucOY67m1O+SkjqePdwA5EUlibaaRBkrfsCU # tNJhbesz2cXfSwQAzH0clcOP9yGyshG3u3/y1YxwLEFgqrFjGESVGnZifvaAsPvo # ZKYz0YkH4b235kOkGLimdwHhD5QMIR2yVCkliWzlDlJRR3S+Jqy2QXXeeqxfjT/J # vNNBERJb5RBQ6zHFynIWIgnffEx1P2PsIV/EIFFrb7GrhotPwtZFX50g/KEexcCP # orF+CiaZ9eRpL5gdLfXZqbId5RsCAwEAAaOCATowggE2MA8GA1UdEwEB/wQFMAMB # Af8wHQYDVR0OBBYEFOzX44LScV1kTN8uZz/nupiuHA9PMB8GA1UdIwQYMBaAFEXr # oq/0ksuCMS1Ri6enIZ3zbcgPMA4GA1UdDwEB/wQEAwIBhjB5BggrBgEFBQcBAQRt # MGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEF # BQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJl # ZElEUm9vdENBLmNydDBFBgNVHR8EPjA8MDqgOKA2hjRodHRwOi8vY3JsMy5kaWdp # Y2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsMBEGA1UdIAQKMAgw # BgYEVR0gADANBgkqhkiG9w0BAQwFAAOCAQEAcKC/Q1xV5zhfoKN0Gz22Ftf3v1cH # vZqsoYcs7IVeqRq7IviHGmlUIu2kiHdtvRoU9BNKei8ttzjv9P+Aufih9/Jy3iS8 # UgPITtAq3votVs/59PesMHqai7Je1M/RQ0SbQyHrlnKhSLSZy51PpwYDE3cnRNTn # f+hZqPC/Lwum6fI0POz3A8eHqNJMQBk1RmppVLC4oVaO7KTVPeix3P0c2PR3WlxU # jG/voVA9/HYJaISfb8rbII01YBwCA8sgsKxYoA5AY8WYIsGyWfVVa88nq2x2zm8j # LfR+cWojayL/ErhULSd+2DrZ8LaHlv1b0VysGMNNn3O3AamfV6peKOK5lDCCBq4w # ggSWoAMCAQICEAc2N7ckVHzYR6z9KGYqXlswDQYJKoZIhvcNAQELBQAwYjELMAkG # A1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRp # Z2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290IEc0MB4X # DTIyMDMyMzAwMDAwMFoXDTM3MDMyMjIzNTk1OVowYzELMAkGA1UEBhMCVVMxFzAV # BgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVk # IEc0IFJTQTQwOTYgU0hBMjU2IFRpbWVTdGFtcGluZyBDQTCCAiIwDQYJKoZIhvcN # AQEBBQADggIPADCCAgoCggIBAMaGNQZJs8E9cklRVcclA8TykTepl1Gh1tKD0Z5M # om2gsMyD+Vr2EaFEFUJfpIjzaPp985yJC3+dH54PMx9QEwsmc5Zt+FeoAn39Q7SE # 2hHxc7Gz7iuAhIoiGN/r2j3EF3+rGSs+QtxnjupRPfDWVtTnKC3r07G1decfBmWN # lCnT2exp39mQh0YAe9tEQYncfGpXevA3eZ9drMvohGS0UvJ2R/dhgxndX7RUCyFo # bjchu0CsX7LeSn3O9TkSZ+8OpWNs5KbFHc02DVzV5huowWR0QKfAcsW6Th+xtVhN # ef7Xj3OTrCw54qVI1vCwMROpVymWJy71h6aPTnYVVSZwmCZ/oBpHIEPjQ2OAe3Vu # JyWQmDo4EbP29p7mO1vsgd4iFNmCKseSv6De4z6ic/rnH1pslPJSlRErWHRAKKtz # Q87fSqEcazjFKfPKqpZzQmiftkaznTqj1QPgv/CiPMpC3BhIfxQ0z9JMq++bPf4O # uGQq+nUoJEHtQr8FnGZJUlD0UfM2SU2LINIsVzV5K6jzRWC8I41Y99xh3pP+OcD5 # sjClTNfpmEpYPtMDiP6zj9NeS3YSUZPJjAw7W4oiqMEmCPkUEBIDfV8ju2TjY+Cm # 4T72wnSyPx4JduyrXUZ14mCjWAkBKAAOhFTuzuldyF4wEr1GnrXTdrnSDmuZDNIz # tM2xAgMBAAGjggFdMIIBWTASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBS6 # FtltTYUvcyl2mi91jGogj57IbzAfBgNVHSMEGDAWgBTs1+OC0nFdZEzfLmc/57qY # rhwPTzAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwgwdwYIKwYB # BQUHAQEEazBpMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20w # QQYIKwYBBQUHMAKGNWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy # dFRydXN0ZWRSb290RzQuY3J0MEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwz # LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRSb290RzQuY3JsMCAGA1UdIAQZ # MBcwCAYGZ4EMAQQCMAsGCWCGSAGG/WwHATANBgkqhkiG9w0BAQsFAAOCAgEAfVmO # wJO2b5ipRCIBfmbW2CFC4bAYLhBNE88wU86/GPvHUF3iSyn7cIoNqilp/GnBzx0H # 6T5gyNgL5Vxb122H+oQgJTQxZ822EpZvxFBMYh0MCIKoFr2pVs8Vc40BIiXOlWk/ # R3f7cnQU1/+rT4osequFzUNf7WC2qk+RZp4snuCKrOX9jLxkJodskr2dfNBwCnzv # qLx1T7pa96kQsl3p/yhUifDVinF2ZdrM8HKjI/rAJ4JErpknG6skHibBt94q6/ae # sXmZgaNWhqsKRcnfxI2g55j7+6adcq/Ex8HBanHZxhOACcS2n82HhyS7T6NJuXdm # kfFynOlLAlKnN36TU6w7HQhJD5TNOXrd/yVjmScsPT9rp/Fmw0HNT7ZAmyEhQNC3 # EyTN3B14OuSereU0cZLXJmvkOHOrpgFPvT87eK1MrfvElXvtCl8zOYdBeHo46Zzh # 3SP9HSjTx/no8Zhf+yvYfvJGnXUsHicsJttvFXseGYs2uJPU5vIXmVnKcPA3v5gA # 3yAWTyf7YGcWoWa63VXAOimGsJigK+2VQbc61RWYMbRiCQ8KvYHZE/6/pNHzV9m8 # BPqC3jLfBInwAM1dwvnQI38AC+R2AibZ8GV2QqYphwlHK+Z/GqSFD/yYlvZVVCsf # gPrA8g4r5db7qS9EFUrnEw4d2zc4GqEr9u3WfPwwggbAMIIEqKADAgECAhAMTWly # S5T6PCpKPSkHgD1aMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNVBAYTAlVTMRcwFQYD # VQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBH # NCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwHhcNMjIwOTIxMDAwMDAw # WhcNMzMxMTIxMjM1OTU5WjBGMQswCQYDVQQGEwJVUzERMA8GA1UEChMIRGlnaUNl # cnQxJDAiBgNVBAMTG0RpZ2lDZXJ0IFRpbWVzdGFtcCAyMDIyIC0gMjCCAiIwDQYJ # KoZIhvcNAQEBBQADggIPADCCAgoCggIBAM/spSY6xqnya7uNwQ2a26HoFIV0Mxom # rNAcVR4eNm28klUMYfSdCXc9FZYIL2tkpP0GgxbXkZI4HDEClvtysZc6Va8z7GGK # 6aYo25BjXL2JU+A6LYyHQq4mpOS7eHi5ehbhVsbAumRTuyoW51BIu4hpDIjG8b7g # L307scpTjUCDHufLckkoHkyAHoVW54Xt8mG8qjoHffarbuVm3eJc9S/tjdRNlYRo # 44DLannR0hCRRinrPibytIzNTLlmyLuqUDgN5YyUXRlav/V7QG5vFqianJVHhoV5 # PgxeZowaCiS+nKrSnLb3T254xCg/oxwPUAY3ugjZNaa1Htp4WB056PhMkRCWfk3h # 3cKtpX74LRsf7CtGGKMZ9jn39cFPcS6JAxGiS7uYv/pP5Hs27wZE5FX/NurlfDHn # 88JSxOYWe1p+pSVz28BqmSEtY+VZ9U0vkB8nt9KrFOU4ZodRCGv7U0M50GT6Vs/g # 9ArmFG1keLuY/ZTDcyHzL8IuINeBrNPxB9ThvdldS24xlCmL5kGkZZTAWOXlLimQ # prdhZPrZIGwYUWC6poEPCSVT8b876asHDmoHOWIZydaFfxPZjXnPYsXs4Xu5zGcT # B5rBeO3GiMiwbjJ5xwtZg43G7vUsfHuOy2SJ8bHEuOdTXl9V0n0ZKVkDTvpd6kVz # HIR+187i1Dp3AgMBAAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/ # BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEE # AjALBglghkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8w # HQYDVR0OBBYEFGKK3tBh/I8xFO2XC809KpQU31KcMFoGA1UdHwRTMFEwT6BNoEuG # SWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQw # OTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQG # CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKG # TGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJT # QTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIB # AFWqKhrzRvN4Vzcw/HXjT9aFI/H8+ZU5myXm93KKmMN31GT8Ffs2wklRLHiIY1UJ # RjkA/GnUypsp+6M/wMkAmxMdsJiJ3HjyzXyFzVOdr2LiYWajFCpFh0qYQitQ/Bu1 # nggwCfrkLdcJiXn5CeaIzn0buGqim8FTYAnoo7id160fHLjsmEHw9g6A++T/350Q # p+sAul9Kjxo6UrTqvwlJFTU2WZoPVNKyG39+XgmtdlSKdG3K0gVnK3br/5iyJpU4 # GYhEFOUKWaJr5yI+RCHSPxzAm+18SLLYkgyRTzxmlK9dAlPrnuKe5NMfhgFknADC # 6Vp0dQ094XmIvxwBl8kZI4DXNlpflhaxYwzGRkA7zl011Fk+Q5oYrsPJy8P7mxNf # arXH4PMFw1nfJ2Ir3kHJU7n/NBBn9iYymHv+XEKUgZSCnawKi8ZLFUrTmJBFYDOA # 4CPe+AOk9kVH5c64A0JH6EE2cXet/aLol3ROLtoeHYxayB6a1cLwxiKoT5u92Bya # UcQvmvZfpyeXupYuhVfAYOd4Vn9q78KVmksRAsiCnMkaBXy6cbVOepls9Oie1FqY # yJ+/jbsYXEP10Cro4mLueATbvdH7WwqocH7wl4R44wgDXUcsY6glOJcB0j862uXl # 9uab3H4szP8XTE0AotjWAQ64i+7m4HJViSwnGWH2dwGMMYIFXTCCBVkCAQEwgYYw # cjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ # d3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVk # IElEIENvZGUgU2lnbmluZyBDQQIQBNXcH0jqydhSALrNmpsqpzANBglghkgBZQME # AgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEM # BgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqG # SIb3DQEJBDEiBCDMh6p58EUQoEKN5mLrsZn/LP9iLR/8m2fwWLnxOYEICzANBgkq # hkiG9w0BAQEFAASCAQC0c+J2fDRDX49PYTn4Mlu3Jp8eWJoc4N5efFYmEW0gLCo4 # WZElwWjEWXrcY33pfGLINLZk14EpcwGQaqez1LdLde0/TFRQwSYZnWKHq+AEAdm/ # HsGzOhGiu0ZzsI+Le8RP8kv0ZxKKZGKYSEudvX8ZgwM2pwk6iqc/yIEn0aZzMLSf # xyFnNCmGRDnJmVDABEn8ppl/HYhb7dKRr1OY+tY21RmNJvpEg8EtoVtyBTj8IYe0 # Hkz7FqN42c1Wi+pChMBtwdcgDoAMj04l9HiFVxlbbZifYgo5o21KCsTlLe0LDe5x # 92/LsBqWUECR7zamYBfcMkoOx2Jr+pTvZCLX9u9voYIDIDCCAxwGCSqGSIb3DQEJ # BjGCAw0wggMJAgEBMHcwYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0 # LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hB # MjU2IFRpbWVTdGFtcGluZyBDQQIQDE1pckuU+jwqSj0pB4A9WjANBglghkgBZQME # AgEFAKBpMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8X # DTIzMDIxNDA4MTg0OVowLwYJKoZIhvcNAQkEMSIEIOzUyzYhQ/ubW5M2BhJzN3Oz # LDFJ65vcwERv/n+VuORYMA0GCSqGSIb3DQEBAQUABIICAD7PYgFwLW6ZVer++mFq # dvk+U+oSAdU6hf9lX4k89NRj5oTVX7lT5gXUDyG7h1pt3tBl+W5VUiKSFEdQ+DA/ # QRDFmVvBUFHxCXItjRwEfhNbyMNqBOPfZMCxZL/smwqgRjam9gH8n5GfsGnfevzF # xVrRMJLEpBrDsmjCZ1z7PdoDiYXIcjNlhLZr423l74GYrW3kuTmLG4tBZjSCTo4C # NVKWiKhKcuEUNISVVCHtUiFa7edT55yqcW7uj2sYYhgFGxZW726K8AG/ftSD4KxL # Qk6BNqWR/bQp+dC0kOyujTzS5VBWtMFwLNXWBdCdSWMzqOiDXydELS9dsKMQjOUz # mCjV56C9msESUKjuqU4yw611s0oBYtlsyX/PirsWPyzomoz14nPL/ZWeT00u6pHY # Ztxb3oyaA0jWkNGdmjnYlbUVsIX4ETFIlky3lp1qDrWinTt2jCfwk857mNRzZxhA # QZ+9SN3uuf1QFHlOYP4DW2VH+56Vx0IeG/6s2p4kPudIiAwM9pQKJDazVMqq89Rj # bIhdpkCNHByb0wXm5azALmBoYyKQPk+1VM4WBaasL+5fBF4O2nlN7d3gHMpKkSuF # r1VRbrVNAhpwiGDFJDthBLdR8Z44+BRuOCnrz1XIh1P14Z9Vl4OXSecMuwPrnMRy # JF9kVpiGyF/SbUVrqbF6SSRa # SIG # End signature block |