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