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 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 Add-FederatedDirectoryUser { [alias('Add-FDUser')] [CmdletBinding()] 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] $EmailAddressWork, [string] $EmailAddressHome, [string] $StreetAddress, [string] $Locality, [string] $Region, [string] $PostalCode, [string] $Country, [string] $StreetAddressHome, [string] $PostalCodeHome, [string] $LocalityHome, [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, [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 ) 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 ($EmailAddressWork) { @{ "value" = $EmailAddressWork "type" = "work" "primary" = $true } } if ($EmailAddressHome) { @{ "value" = $EmailAddressHome "type" = "home" } } ) "addresses" = @( if ($StreetAddress -or $Locality -or $Region -or $PostalCode -or $Country) { @{ "streetAddress" = $StreetAddress "locality" = $Locality "region" = $Region "postalCode" = $PostalCode "country" = $Country "type" = "work" "primary" = $true } } if ($StreetAddressHome -or $LocalityHome -or $RegionHome -or $PostalCodeHome -or $CountryHome) { @{ "streetAddress" = $StreetAddressHome "locality" = $LocalityHome "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" = @( @{ "value" = $Role "display" = $Role } ) "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" = [ordered] @{ "organization" = $Organization "department" = $Department "employeeNumber" = $EmployeeNumber "costCenter" = $CostCenter "division" = $Division "manager" = @{ "displayName" = $ManagerDisplayName "value" = $ManagerID "`$ref" = $ManagerReference # "../v2/Users/d09420a0-e97a-11e7-9faf-236ea7c81614" } } "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 $invokeRestMethodSplat = [ordered] @{ Method = 'POST' Uri = 'https://api.federated.directory/v2/Users' Headers = [ordered] @{ 'Content-Type' = 'application/json' 'Authorization' = $Authorization.Authorization 'Cache-Control' = 'no-cache' } Body = $Body | ConvertTo-Json -Depth 10 ErrorAction = 'Stop' } if ($DirectoryID) { $invokeRestMethodSplat['Headers']['directoryId'] = $DirectoryID } # for troubleshooting if ($VerbosePreference -eq 'Continue') { $Body | ConvertTo-Json -Depth 10 | Write-Verbose } $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 { [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 = $ClientSecretEncrypted | ConvertTo-SecureString -ErrorAction Stop } catch { if ($PSBoundParameters.ErrorAction -eq 'Stop') { throw } else { $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " " Write-Warning -Message "Connect-ILM - 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-ILM - 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 { [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, [string] $UserName, [string] $DirectoryID, [int] $MaxResults, [int] $StartIndex = 1, [int] $Count = 50, [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 ) $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 ($UserName) { # keep in mind regardless of used operator it will always revert back to co as per API (weird) "userName co $UserName" } 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' 'Authorization' = $Authorization.Authorization 'directoryID' = $DirectoryID } Remove-EmptyValue -Hashtable $Headers Try { $BatchObjects = Invoke-RestMethod -Method Get -Uri $Uri -Headers $Headers -ErrorAction Stop #{id}?attributes={attributes}' } 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 $BatchObjects.Resources | Select-Object -First $MaxResults $LimitReached = $true } else { # return all users that were given in a batch $BatchObjects.Resources } } 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 -join "," DirectoryID = $DirectoryID } Remove-EmptyValue -Hashtable $getFederatedDirectoryUserSplat Get-FederatedDirectoryUser @getFederatedDirectoryUserSplat } } else { Write-Warning -Message 'Get-FederatedDirectoryUser - No authorization found. Please make sure to use Connect-FederatedDirectory first.' } } # $Script:AttributesList = @( # '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' # ) # $Script:ScriptBlockAttributes = { # param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) # $Script:AttributesList | Where-Object { $_ -like "*$wordToComplete*" } # } # Register-ArgumentCompleter -CommandName Get-FederatedDirectoryUser -ParameterName Attributes -ScriptBlock $Script:ScriptBlockAttributes # Register-ArgumentCompleter -CommandName Get-FederatedDirectoryUser -ParameterName SortBy -ScriptBlock $Script:ScriptBlockAttributes 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 UserName 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. .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')] 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[]] $UserName, [parameter()][string] $DirectoryID ) 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 "Get-FederatedDirectory - No authorization found. Please run 'Connect-FederatedDirectory' first." return } } } } Process { if ($Authorization) { if ($Id) { $RemoveID = $Id } elseif ($User) { $RemoveID = $User.Id } elseif ($UserName) { $RemoveID = Foreach ($U in $UserName) { (Get-FederatedDirectoryUser -Authorization $Authorization -UserName $U).Id } } else { return } foreach ($I in $RemoveID) { Try { $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' } Remove-EmptyValue -Hashtable $invokeRestMethodSplat if ($VerbosePreference -eq 'Continue') { $invokeRestMethodSplat | ConvertTo-Json -Depth 10 | Write-Verbose } $ReturnData = Invoke-RestMethod @invokeRestMethodSplat $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()] param( [System.Collections.IDictionary] $Authorization, [string] $Id, [string] $ExternalId, [parameter()][string] $DirectoryID, [parameter()][string] $UserName, [Alias('FirstName')] $FamilyName, [string] $GivenName, [parameter()][string] $DisplayName, [string] $NickName, [string] $ProfileUrl, [string] $EmailAddressWork, [string] $EmailAddressHome, [string] $StreetAddress, [string] $Locality, [string] $Region, [string] $PostalCode, [string] $Country, [string] $StreetAddressHome, [string] $PostalCodeHome, [string] $LocalityHome, [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] $Department, [string] $EmployeeNumber, [string] $CostCenter, [string] $Division, [string] $Description, [ValidateSet('admin', 'user')][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 = @{} ) 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 ($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' EmailAddressWork = 'emails[type eq "work"].value' EmailAddressHome = 'emails[type eq "home"].value' StreetAddress = 'addresses[type eq "work"].streetAddress' Locality = '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' LocalityHome = '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 = 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager.value' ManagerUserName = 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager.value' ManagerReference = 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager.$ref' ManagerDisplayName = 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager.displayName' 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' } if ($ManagerUserName) { $ManagerID = (Get-FederatedDirectoryUser -Authorization $Authorization -UserName $ManagerUserName).Id } $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 ($PSBoundParameters[$Key]) { $Value = $PSBoundParameters[$Key] } else { $Value = $null } if ($ActionPerProperty) { if ($ActionPerProperty[$Key]) { $ActionProperty = $ActionPerProperty[$Key] } else { $ActionProperty = 'replace' } } else { $ActionProperty = 'replace' } [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 ($EmailAddressWork) { @{ "value" = $EmailAddressWork "type" = "work" "primary" = $true } } if ($EmailAddressHome) { @{ "value" = $EmailAddressHome "type" = "home" } } ) "addresses" = @( if ($StreetAddress -or $Locality -or $Region -or $PostalCode -or $Country) { @{ "streetAddress" = $StreetAddress "locality" = $Locality "region" = $Region "postalCode" = $PostalCode "country" = $Country "type" = "work" "primary" = $true } } if ($StreetAddressHome -or $LocalityHome -or $RegionHome -or $PostalCodeHome -or $CountryHome) { @{ "streetAddress" = $StreetAddressHome "locality" = $LocalityHome "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" = @( @{ "value" = $Role "display" = $Role } ) "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" = [ordered] @{ "organization" = $Organization "department" = $Department "employeeNumber" = $EmployeeNumber "costCenter" = $CostCenter "division" = $Division "manager" = @{ "displayName" = $ManagerDisplayName "value" = $ManagerID "`$ref" = $ManagerReference # "../v2/Users/d09420a0-e97a-11e7-9faf-236ea7c81614" } } "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 $invokeRestMethodSplat = [ordered] @{ Method = if ($Action -eq 'Update') { 'PATCH' } else { 'PUT' } Uri = "https://api.federated.directory/v2/Users/$Id" Headers = [ordered] @{ 'Content-Type' = 'application/json' 'Authorization' = $Authorization.Authorization 'Cache-Control' = 'no-cache' } Body = $Body | ConvertTo-Json -Depth 10 ErrorAction = 'Stop' } if ($DirectoryID) { $invokeRestMethodSplat['Headers']['directoryId'] = $DirectoryID } # for troubleshooting if ($VerbosePreference -eq 'Continue') { $Body | ConvertTo-Json -Depth 10 | Write-Verbose } $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: $Id]" } else { Write-Warning -Message "Set-FederatedDirectoryUser - Error $($_.Exception.Message), $($ErrorDetails.Detail)" } } } } 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', 'Remove-FederatedDirectoryUser', 'Set-FederatedDirectoryUser') -Alias @('Add-FDUser', 'Connect-FD', 'Get-FDSchema', 'Get-FDUser', 'Remove-FDUser', 'Set-FDUser') # SIG # Begin signature block # MIInRAYJKoZIhvcNAQcCoIInNTCCJzECAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCB1wIWAXPUJxbaR # 3bdNBt+vfeumfX9y42nd60Owx3dTZaCCIT0wggO3MIICn6ADAgECAhAM5+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 # gPrA8g4r5db7qS9EFUrnEw4d2zc4GqEr9u3WfPwwggbGMIIErqADAgECAhAKekqI # nsmZQpAGYzhNhpedMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNVBAYTAlVTMRcwFQYD # VQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBH # NCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwHhcNMjIwMzI5MDAwMDAw # WhcNMzMwMzE0MjM1OTU5WjBMMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNl # cnQsIEluYy4xJDAiBgNVBAMTG0RpZ2lDZXJ0IFRpbWVzdGFtcCAyMDIyIC0gMjCC # AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALkqliOmXLxf1knwFYIY9DPu # zFxs4+AlLtIx5DxArvurxON4XX5cNur1JY1Do4HrOGP5PIhp3jzSMFENMQe6Rm7p # o0tI6IlBfw2y1vmE8Zg+C78KhBJxbKFiJgHTzsNs/aw7ftwqHKm9MMYW2Nq867Lx # g9GfzQnFuUFqRUIjQVr4YNNlLD5+Xr2Wp/D8sfT0KM9CeR87x5MHaGjlRDRSXw9Q # 3tRZLER0wDJHGVvimC6P0Mo//8ZnzzyTlU6E6XYYmJkRFMUrDKAz200kheiClOEv # A+5/hQLJhuHVGBS3BEXz4Di9or16cZjsFef9LuzSmwCKrB2NO4Bo/tBZmCbO4O2u # fyguwp7gC0vICNEyu4P6IzzZ/9KMu/dDI9/nw1oFYn5wLOUrsj1j6siugSBrQ4nI # fl+wGt0ZvZ90QQqvuY4J03ShL7BUdsGQT5TshmH/2xEvkgMwzjC3iw9dRLNDHSNQ # zZHXL537/M2xwafEDsTvQD4ZOgLUMalpoEn5deGb6GjkagyP6+SxIXuGZ1h+fx/o # K+QUshbWgaHK2jCQa+5vdcCwNiayCDv/vb5/bBMY38ZtpHlJrYt/YYcFaPfUcONC # leieu5tLsuK2QT3nr6caKMmtYbCgQRgZTu1Hm2GV7T4LYVrqPnqYklHNP8lE54CL # KUJy93my3YTqJ+7+fXprAgMBAAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYD # VR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgG # BmeBDAEEAjALBglghkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxq # II+eyG8wHQYDVR0OBBYEFI1kt4kh/lZYRIRhp+pvHDaP3a8NMFoGA1UdHwRTMFEw # T6BNoEuGSWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRH # NFJTQTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGD # MIGAMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYB # BQUHMAKGTGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0 # ZWRHNFJTQTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQEL # BQADggIBAA0tI3Sm0fX46kuZPwHk9gzkrxad2bOMl4IpnENvAS2rOLVwEb+EGYs/ # XeWGT76TOt4qOVo5TtiEWaW8G5iq6Gzv0UhpGThbz4k5HXBw2U7fIyJs1d/2Wcuh # wupMdsqh3KErlribVakaa33R9QIJT4LWpXOIxJiA3+5JlbezzMWn7g7h7x44ip/v # EckxSli23zh8y/pc9+RTv24KfH7X3pjVKWWJD6KcwGX0ASJlx+pedKZbNZJQfPQX # podkTz5GiRZjIGvL8nvQNeNKcEiptucdYL0EIhUlcAZyqUQ7aUcR0+7px6A+TxC5 # MDbk86ppCaiLfmSiZZQR+24y8fW7OK3NwJMR1TJ4Sks3KkzzXNy2hcC7cDBVeNaY # /lRtf3GpSBp43UZ3Lht6wDOK+EoojBKoc88t+dMj8p4Z4A2UKKDr2xpRoJWCjihr # pM6ddt6pc6pIallDrl/q+A8GQp3fBmiW/iqgdFtjZt5rLLh4qk1wbfAs8QcVfjW0 # 5rUMopml1xVrNQ6F1uAszOAMJLh8UgsemXzvyMjFjFhpr6s94c/MfRWuFL+Kcd/K # l7HYR+ocheBFThIcFClYzG/Tf8u+wQ5KbyCcrtlzMlkI5y2SoRoR/jKYpl0rl+CL # 05zMbbUNrkdjOEcXW28T2moQbh9Jt0RbtAgKh1pZBHYRoad3AhMcMYIFXTCCBVkC # AQEwgYYwcjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcG # A1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBB # c3N1cmVkIElEIENvZGUgU2lnbmluZyBDQQIQBNXcH0jqydhSALrNmpsqpzANBglg # hkgBZQMEAgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3 # DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEV # MC8GCSqGSIb3DQEJBDEiBCAxJdh2vLBHKz+dcz4PAWxFsoSqL7AVIAASfMuDhGXL # VTANBgkqhkiG9w0BAQEFAASCAQB7v4/KopP9iEE4/jQcBvVTs5NNqla63HqCZrze # SeiT3ugzBke/BEze5mCvHXUuA5reChl5L1Q5iYfdnzsrkNesRrb3bcls5CTJrbGA # sn3SIeh5JjiQ/63BJmxF3OBOG1XPA5KJ8XBXD9Idw4uHK2XaWHlbcEyI+5pcLgHV # GE90z9GZ1XacjlzhkVIfZcc1lZTMoLIf3iaXhcvYvdfFOk4SUL/UyFNh8S6zlqpc # cNPIhdJM38uTUhi6DHNWSQY9Ph0ehCs9JiFJcqLsoH8T2utkbZl4Sqsv2IQWiBoW # R93YdUIasSIIfoeLyZLUJv3MecaLcsjCt+h6zxTDEFL8G6vYoYIDIDCCAxwGCSqG # SIb3DQEJBjGCAw0wggMJAgEBMHcwYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRp # Z2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQw # OTYgU0hBMjU2IFRpbWVTdGFtcGluZyBDQQIQCnpKiJ7JmUKQBmM4TYaXnTANBglg # hkgBZQMEAgEFAKBpMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcN # AQkFMQ8XDTIyMDkxMTEyNTY0OFowLwYJKoZIhvcNAQkEMSIEIE8ynnYKoG87k9i6 # n52/mhbFiwYtrvt1KRDvq/cfXInKMA0GCSqGSIb3DQEBAQUABIICAFshh/VwIAsX # bNdka9V/iXtg5j9pgTgmWaJb7g/OYN+6sZeEYMjrNICU7xEcNIoZoeoHPpbe8p7Z # qi5BZDLZZxUKPPJ+JOyjr3xfnuNSS1ld6ecBvYOSnnkbfwcfEK8/L0+SrLwKL+I9 # SAeW+WGlLGJHGhOXsq4jlX2co6VQxnBWSiSzToPveGNMs1mWl/2/6OQq7029CtuN # 0mwBsef0W9mTECD79zspLwTRT+Cn3rDk3yemtZagV1tJxuKON0JJWTdPFFSyRzJY # PYZHDsg9OPPJwETLaVPjXqGcTHAzx7ZfmBvZ5vRXaohrLeQiCgDc1DObSv/B1c/g # TO1Asmqb6hLVKSBqVKYwQlup7VbUh0lEjSsF20RBsDdIXV5VELPioTjO40FYHCye # HNrLzv8izxDs/XO0QCHtNbkV8ImB1hp5gm5S4gHw1rqlWaWXacKz60J/phLjVbI1 # 5iMZt2lqrDIf62UMajP6TQe5AOHv0AkJDUr3LIsxVeGEmlFTJ8ZplIZuTIb/Rssv # NRC4OabNHJS1BXoBRaFHfBxltSP5BrPotuZQ/o3WSNXY8sJXZSMjQ/KZrX9AyiLS # Sj9aWDETqhF0R/nj29sZGQnoN9iYhVoM+pYemkLxwh5K2y1/uSx5aOIojabohxaz # eCgHjFETY8ghKfC/gCHgCA/IYwsBWK8D # SIG # End signature block |