AzureValidation/AzureADConfiguration.psm1

<#############################################################
# #
# Copyright (C) Microsoft Corporation. All rights reserved. #
# #
#############################################################>


$ErrorActionPreference = 'Stop'

Import-LocalizedData LocalizedData -BaseDirectory $PSScriptRoot -Filename AzureADConfiguration.Strings.psd1

$AzurePowerShellClientID = "1950a258-227b-4e31-a9cf-717495945fc2"
$AzurePowerShellReturnUri = "urn:ietf:wg:oauth:2.0:oob"

function Get-Endpoints([string] $CloudARMEndpoint) {
    <#
    .Synopsis
    Builds graph and login endpoints for a given CloudARMEndpoint
    #>


    $fullUri = $CloudARMEndpoint.TrimEnd('/')+"/metadata/endpoints?api-version=2015-01-01"
    Write-AzsReadinessLog -message "Getting Azure Endpoints from $fullUri" -type Info
    $response = Invoke-RestMethod -Uri $fullUri -ErrorAction Stop -UseBasicParsing -TimeoutSec 30

    $EndpointProperties = @{
        GraphUri = $response.graphEndpoint
        LoginUri = $response.authentication.loginEndpoint
        ManagementServiceUri = $response.authentication.audiences[0]
        ARMUri = $CloudARMEndpoint
    }

    return $EndpointProperties
}

function Get-AADTenantIds
{
    [OutputType([Hashtable])]
    [CmdletBinding()]
    param()

    $token = Get-AzAccessToken
    $cToken = ConvertFrom-JwtToken $token.Token

    $UserInfo = @{
        FirstName = $cToken.claims.given_name
        LastName = $cToken.claims.family_name
        Account = $cToken.claims.unique_name
    }

    Write-AzsReadinessLog -message "Retrieving tenantIds..." -type Info
    $tenantsResponse = Invoke-RestMethod -Method Get -Uri "$($AzureURIs.ARMUri.TrimEnd('/'))/tenants?api-version=2016-02-01" -Headers @{Authorization = "Bearer $($token.Token)"} -UseBasicParsing -TimeoutSec 30
    $tenantIds = [Array]$tenantsResponse.Value.tenantId

    return @{
                UserInfo = $UserInfo
                TenantIds = $tenantIds
            }
}

function Invoke-Graph($method, $uri, $authorization, $body)
{

    $params = @{
        Method  = $method
        Uri     = "$($uri)?api-version=1.6"
        Headers = @{ Authorization = $authorization }
        UseBasicParsing = $true
        TimeoutSec = 30
        ContentType = 'application/json'
    }
    if ($body) { $params += @{ Body = $body } }

    try
    {
        return (Invoke-WebRequest @params -ErrorAction Stop).Content | ConvertFrom-Json
    }
    catch
    {
        if ($_.Exception.Response.StatusCode -eq 'Internal Server Error')
        {
            Write-AzsReadinessLog -message ("$LocalizedData.GraphEndpointError" -f $graphUri) -type Error
            throw ($LocalizedData.GraphEndpointError -f $graphUri)
        }
        if ($_.Exception.Response.StatusCode -eq 'Forbidden')
        {
            Write-AzsReadinessLog -message ("{0} StatusCode: Forbidden" -f $graphUri) -type Error
            throw "Forbidden: Login using Connect-AzAccount and specify the target tenant."
        }
        else
        {
            Write-AzsReadinessLog -message ("{0} throw an exception: {1}" -f $graphUri, $_) -type Error
            throw $_
        }
    }

}

function Get-AADTenantDetail
{
    [OutputType([Hashtable])]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        $TenantId,

        [Parameter(Mandatory=$true)]
        [string]
        $UserId,

        [Parameter(Mandatory=$true)]
        [Hashtable]
        $AzureURIs
    )

    try
    {
        $token = Get-AzAccessToken -resource (Get-AzContext).Environment.GraphEndpointResourceId -TenantId $TenantId
    }
    catch {
            throw $_
    }

    # Validating if the user has required permission
    $authorization = "Bearer $($token.Token)"

    $accessTokenObj = ConvertFrom-JwtToken -JwtToken $token.Token
    $postUri = $azureURIs.GraphUri.TrimEnd('/')+"/$($TenantId)/directoryObjects/$($accessTokenObj.Claims.oid)/getMemberObjects"
    Write-AzsReadinessLog -message "Retrieving member objects for user." -type Info
    $roles = Invoke-Graph -method Post -uri $postUri -authorization $authorization -body ( ConvertTo-Json -Depth 1 @{ securityEnabledOnly = $false } ) | Select-Object -ExpandProperty value
    if( $roles -eq $null ){ return $null }

    $getUri = $azureURIs.GraphUri.TrimEnd('/')+"/$($TenantId)/directoryRoles"
    Write-AzsReadinessLog -message "Retrieving administrator role in directory." -type Info
    $roleOid = Invoke-Graph -method Get -uri $getUri -authorization $authorization | Select-Object -ExpandProperty Value | Where { ($_.displayName -In 'Company Administrator', 'Global Administrator') -or ($_.roleTemplateId -eq '62e90394-69f5-4237-9190-012177145e10') } | Select-Object -ExpandProperty objectId

    if ($roleOid -notin $roles) {
        Write-AzsReadinessLog -message "Administrator role not present in user roles." -type Error
        return $null
    }
    Write-AzsReadinessLog -message "Success. Administrator role present in user roles." -type Info
    $tenantResponse = Invoke-RestMethod -Method Get -Uri "$($AzureURIs.GraphUri.TrimEnd('/'))/myOrganization/tenantDetails?api-version=1.5" -Headers @{Authorization = "Bearer $($token.Token)"} -UseBasicParsing -TimeoutSec 30

    return @{
                Id = $tenantResponse.Value.objectId
                DisplayName = $tenantResponse.Value.DisplayName
                DomainName = ($tenantResponse.Value.VerifiedDomains | Where {$_.default}).name
                Token = $token.Token
            }
}

function Get-AADTenantDetails
{
    <#
    .SYNOPSIS
        Resolve Azure Tenant Details
    .DESCRIPTION
        Resolve Azure Tenant Details
    .EXAMPLE
        Get-AADTenantDetails -AADDirectoryTenantName azurestack.contoso.com
    .OUTPUTS
        Hashtable of tenant details
    .NOTES
        General notes
    #>

    [OutputType([Hashtable])]
    [CmdletBinding()]
    param(

        [Parameter(Mandatory=$false)]
        [string] $AADDirectoryTenantName

    )

    Write-Progress -Activity $LocalizedData.InfoSigningIn

    $azureURIs = Get-Endpoints (Get-AzContext).Environment.ResourceManagerUrl
    $TenantsInfo = Get-AADTenantIds
    $TenantIds = [Array] $TenantsInfo.TenantIds
    $tenantDetails = @()
    # Workaround to add the target tenant Id if specified to the list of "resolved" tenant memberships
    if ($AADDirectoryTenantName)
    {
        Write-AzsReadinessLog -message "Retrieving tenant detail from target tenant $AADDirectoryTenantName" -type Info
        $targetTenantId = Get-TenantIdFromName -tenantName $AADDirectoryTenantName
        $tenantDetail =  Get-AADTenantDetail -TenantId $targetTenantId -UserId $TenantsInfo.UserInfo.Account -AzureURIs $azureURIs
        if ($tenantDetail -ne $null)
        {
            Write-AzsReadinessLog -message ("Success. Target directory ({0}...{1}) returned result." -f $targetTenantId.split('-')[0],$targetTenantId.split('-')[-1]) -type Info
            $tenantDetails = $tenantDetail
        }
        else {
            for ($i = 1; $i -le $TenantIds.Count; $i++)
            {
                Write-Progress -Activity $LocalizedData.InfoSigningIn -PercentComplete ($i * 100 / ($TenantIds.Count + 1))
                $tid = $TenantIds[$i-1]
                Write-AzsReadinessLog -message ("Retrieving tenant detail from tenant {0}...{1}" -f $tid.split('-')[0],$tid.split('-')[-1]) -type Info
                $tenantDetail =  Get-AADTenantDetail -TenantId $tid -UserId $TenantsInfo.UserInfo.Account -AzureURIs $azureURIs
                if ($tenantDetail -ne $null)
                {
                    Write-AzsReadinessLog -message ("Success. Directory ({0}...{1}) returned result." -f $tid.split('-')[0],$tid.split('-')[-1]) -type Info
                    $tenantDetails += $tenantDetail
                }
            }
        }
    }

    Write-Progress -Activity $LocalizedData.InfoSigningIn -Completed

    return @{
                UserInfo = $TenantsInfo.UserInfo
                TenantDetails = $tenantDetails
            }
}

<#
.Synopsis
Decodes a base64-encoded string.
#>

function ConvertFrom-Base64String
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateNotNull()]
        [string] $Base64String
    )
    process
    {
        $data = $Base64String.Replace('-','+').Replace('_','/')
        switch ($data.Length % 4)
        {
            0 { break }
            2 { $data += '==' }
            3 { $data += '=' }
            default { Write-Error "Invalid data: '$data'" }
    }
        $bytes = [System.Convert]::FromBase64String($data)
        Write-Output $bytes
    }
}

<#
.Synopsis
Converts a Jwt Token into an object.
#>

function ConvertFrom-JwtToken
{
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string] $JwtToken
    )

    function ConvertFrom-RawData([String]$Data)
    {
        [System.Text.Encoding]::UTF8.GetString((ConvertFrom-Base64String $Data)) | ConvertFrom-Json | Write-Output
    }

    $parts = $JwtToken.Split('.')
    [pscustomobject]@{
        Headers = ConvertFrom-RawData $parts[0]
        Claims = ConvertFrom-RawData $parts[1]
        Signature = $parts[2]
    } | Write-Output
}

function Get-AzureADTenantDetails
{
    <#
    .SYNOPSIS
        Resolve Azure AD Tenant Details
    .DESCRIPTION
        Resolve Azure AD Tenant Details
    .EXAMPLE
        Get-AzureADTenantDetails -AADDirectoryTenantName $AADDirectoryTenantName
    .INPUTS
        AzureEnvironment - string - should be AzureCloud, AzureChinaCloud, AzureGermanCloud or CustomCloud
        AADAdminCredential - PSCredential - AAD Admin Credential
        AADDirectoryTenantName - string - AAD tenant Name
    .OUTPUTS
        Hashtable of Azure AD tenant details
    .NOTES
        General notes
    #>

    [OutputType([Hashtable])]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$false)]
        [string] $AADDirectoryTenantName
    )

    $claimsProvider = (Get-AzContext).Environment.Name

    $azureResult = Get-AADTenantDetails -AADDirectoryTenantName $AADDirectoryTenantName
    $azureToken = $null
    #$azureRefreshToken = $null
    $tenantId = $null
    $tenantName = $null

    if ($azureResult.TenantDetails -eq $null -or @($azureResult.TenantDetails).Count -eq 0)
    {
        Write-AzsReadinessLog -message ($LocalizedData.AADAccountNotAdmin -f $($azureResult.UserInfo.Account)) -type Error -toScreen
    }
    elseif (@($azureResult.TenantDetails).Count -gt 1 -and -not $AADDirectoryTenantName)
    {
        Write-AzsReadinessLog -message ($LocalizedData.MoreThanOneTenant -f @($($azureResult.UserInfo.Account), $($azureResult.TenantDetails.DomainName -join ', '))) -type Error -toScreen
    }
    elseif (@($azureResult.TenantDetails).Count -ige 1 -and $AADDirectoryTenantName)
    {
        $tenantDetail = $azureResult.TenantDetails | Where-Object DomainName -eq $AADDirectoryTenantName
        if ($tenantDetail)
        {
            $azureToken = $tenantDetail.Token
            #$azureRefreshToken = $tenantDetail.RefreshToken
            $tenantName = $tenantDetail.DomainName
            $tenantID = $tenantDetail.ID
        }
        else
        {
            Write-AzsReadinessLog -message ($LocalizedData.NotAdminOfTenant -f @($($azureResult.UserInfo.Account), $AADDirectoryTenantName, $($azureResult.TenantDetails.DomainName -join ', '))) -type Error -toScreen
        }
    }
    else
    {
        $azureToken = $azureResult.TenantDetails.Token
        #$azureRefreshToken = $azureResult.TenantDetails.RefreshToken
        $tenantName = $azureResult.TenantDetails.DomainName
        $tenantID = $azureResult.TenantDetails.ID
    }

    $adminSubscriptionOwner = (ConvertFrom-JwtToken $azureToken).Claims.unique_name

    return @{
                UserName = $azureResult.UserInfo.Account
                Token = $azureToken
                #RefreshToken = $azureRefreshToken
                AdminSubscriptionOwner = $adminSubscriptionOwner
                TenantDirectoryName = $tenantName
                TenantDirectoryID = $tenantID
                ClaimsProvider = $claimsProvider
            }
}

<#
.SYNOPSIS
    Returns Azure AD directory tenant ID given the login endpoint and the directory tenant name
.DESCRIPTION
    Makes an unauthenticated REST call to the given Azure environment's login endpoint to retrieve directory tenant id
.EXAMPLE
$tenantId = Get-TenantIdFromName -azureEnvironment "Public Azure" -tenantName "msazurestack.onmicrosoft.com"
#>

function Get-TenantIdFromName
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        [string] $tenantName
     )

    $azureURIs = Get-Endpoints (Get-AzContext).Environment.ResourceManagerUrl
    $uri = "{0}/{1}/.well-known/openid-configuration" -f ($azureURIs.LoginUri).TrimEnd('/'), $tenantName

    $response = Invoke-RestMethod -Uri $uri -Method Get -UseBasicParsing -TimeoutSec 30

    Write-AzsReadinessLog -message "using token_endpoint $($response.token_endpoint) to parse tenant id" -type Info
    $tenantId = $response.token_endpoint.Split('/')[3]

    $tenantIdGuid = [guid]::NewGuid()
    $result = [guid]::TryParse($tenantId, [ref] $tenantIdGuid)

    if(-not $result)
    {
        Write-AzsReadinessLog -message "Error obtaining tenant id from tenant name" -type Error
    }
    else
    {
        Write-AzsReadinessLog -message "Success. Tenant Name: $tenantName Tenant id: $tenantId" -type Info
        return $tenantId
    }
}

Export-ModuleMember -Function Get-AzureADTenantDetails
Export-ModuleMember -Function Get-Endpoints
Export-ModuleMember -Function Get-TenantIdFromName
Export-ModuleMember -Function ConvertFrom-JwtToken
# SIG # Begin signature block
# MIIjnwYJKoZIhvcNAQcCoIIjkDCCI4wCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCQHjZZNP6n8+Nn
# W6GJs/XSoIb0Nb7TQtXoWxVGvSm0m6CCDYEwggX/MIID56ADAgECAhMzAAAB32vw
# LpKnSrTQAAAAAAHfMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjAxMjE1MjEzMTQ1WhcNMjExMjAyMjEzMTQ1WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQC2uxlZEACjqfHkuFyoCwfL25ofI9DZWKt4wEj3JBQ48GPt1UsDv834CcoUUPMn
# s/6CtPoaQ4Thy/kbOOg/zJAnrJeiMQqRe2Lsdb/NSI2gXXX9lad1/yPUDOXo4GNw
# PjXq1JZi+HZV91bUr6ZjzePj1g+bepsqd/HC1XScj0fT3aAxLRykJSzExEBmU9eS
# yuOwUuq+CriudQtWGMdJU650v/KmzfM46Y6lo/MCnnpvz3zEL7PMdUdwqj/nYhGG
# 3UVILxX7tAdMbz7LN+6WOIpT1A41rwaoOVnv+8Ua94HwhjZmu1S73yeV7RZZNxoh
# EegJi9YYssXa7UZUUkCCA+KnAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUOPbML8IdkNGtCfMmVPtvI6VZ8+Mw
# UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1
# ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDYzMDA5MB8GA1UdIwQYMBaAFEhu
# ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu
# bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w
# Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3
# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx
# MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAnnqH
# tDyYUFaVAkvAK0eqq6nhoL95SZQu3RnpZ7tdQ89QR3++7A+4hrr7V4xxmkB5BObS
# 0YK+MALE02atjwWgPdpYQ68WdLGroJZHkbZdgERG+7tETFl3aKF4KpoSaGOskZXp
# TPnCaMo2PXoAMVMGpsQEQswimZq3IQ3nRQfBlJ0PoMMcN/+Pks8ZTL1BoPYsJpok
# t6cql59q6CypZYIwgyJ892HpttybHKg1ZtQLUlSXccRMlugPgEcNZJagPEgPYni4
# b11snjRAgf0dyQ0zI9aLXqTxWUU5pCIFiPT0b2wsxzRqCtyGqpkGM8P9GazO8eao
# mVItCYBcJSByBx/pS0cSYwBBHAZxJODUqxSXoSGDvmTfqUJXntnWkL4okok1FiCD
# Z4jpyXOQunb6egIXvkgQ7jb2uO26Ow0m8RwleDvhOMrnHsupiOPbozKroSa6paFt
# VSh89abUSooR8QdZciemmoFhcWkEwFg4spzvYNP4nIs193261WyTaRMZoceGun7G
# CT2Rl653uUj+F+g94c63AhzSq4khdL4HlFIP2ePv29smfUnHtGq6yYFDLnT0q/Y+
# Di3jwloF8EWkkHRtSuXlFUbTmwr/lDDgbpZiKhLS7CBTDj32I0L5i532+uHczw82
# oZDmYmYmIUSMbZOgS65h797rj5JJ6OkeEUJoAVwwggd6MIIFYqADAgECAgphDpDS
# AAAAAAADMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMK
# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0
# IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0
# ZSBBdXRob3JpdHkgMjAxMTAeFw0xMTA3MDgyMDU5MDlaFw0yNjA3MDgyMTA5MDla
# MH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMT
# H01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTEwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQCr8PpyEBwurdhuqoIQTTS68rZYIZ9CGypr6VpQqrgG
# OBoESbp/wwwe3TdrxhLYC/A4wpkGsMg51QEUMULTiQ15ZId+lGAkbK+eSZzpaF7S
# 35tTsgosw6/ZqSuuegmv15ZZymAaBelmdugyUiYSL+erCFDPs0S3XdjELgN1q2jz
# y23zOlyhFvRGuuA4ZKxuZDV4pqBjDy3TQJP4494HDdVceaVJKecNvqATd76UPe/7
# 4ytaEB9NViiienLgEjq3SV7Y7e1DkYPZe7J7hhvZPrGMXeiJT4Qa8qEvWeSQOy2u
# M1jFtz7+MtOzAz2xsq+SOH7SnYAs9U5WkSE1JcM5bmR/U7qcD60ZI4TL9LoDho33
# X/DQUr+MlIe8wCF0JV8YKLbMJyg4JZg5SjbPfLGSrhwjp6lm7GEfauEoSZ1fiOIl
# XdMhSz5SxLVXPyQD8NF6Wy/VI+NwXQ9RRnez+ADhvKwCgl/bwBWzvRvUVUvnOaEP
# 6SNJvBi4RHxF5MHDcnrgcuck379GmcXvwhxX24ON7E1JMKerjt/sW5+v/N2wZuLB
# l4F77dbtS+dJKacTKKanfWeA5opieF+yL4TXV5xcv3coKPHtbcMojyyPQDdPweGF
# RInECUzF1KVDL3SV9274eCBYLBNdYJWaPk8zhNqwiBfenk70lrC8RqBsmNLg1oiM
# CwIDAQABo4IB7TCCAekwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFEhuZOVQ
# BdOCqhc3NyK1bajKdQKVMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1Ud
# DwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFHItOgIxkEO5FAVO
# 4eqnxzHRI4k0MFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwubWljcm9zb2Z0
# LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
# Mi5jcmwwXgYIKwYBBQUHAQEEUjBQME4GCCsGAQUFBzAChkJodHRwOi8vd3d3Lm1p
# Y3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
# Mi5jcnQwgZ8GA1UdIASBlzCBlDCBkQYJKwYBBAGCNy4DMIGDMD8GCCsGAQUFBwIB
# FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2RvY3MvcHJpbWFyeWNw
# cy5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AcABvAGwAaQBjAHkA
# XwBzAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAGfyhqWY
# 4FR5Gi7T2HRnIpsLlhHhY5KZQpZ90nkMkMFlXy4sPvjDctFtg/6+P+gKyju/R6mj
# 82nbY78iNaWXXWWEkH2LRlBV2AySfNIaSxzzPEKLUtCw/WvjPgcuKZvmPRul1LUd
# d5Q54ulkyUQ9eHoj8xN9ppB0g430yyYCRirCihC7pKkFDJvtaPpoLpWgKj8qa1hJ
# Yx8JaW5amJbkg/TAj/NGK978O9C9Ne9uJa7lryft0N3zDq+ZKJeYTQ49C/IIidYf
# wzIY4vDFLc5bnrRJOQrGCsLGra7lstnbFYhRRVg4MnEnGn+x9Cf43iw6IGmYslmJ
# aG5vp7d0w0AFBqYBKig+gj8TTWYLwLNN9eGPfxxvFX1Fp3blQCplo8NdUmKGwx1j
# NpeG39rz+PIWoZon4c2ll9DuXWNB41sHnIc+BncG0QaxdR8UvmFhtfDcxhsEvt9B
# xw4o7t5lL+yX9qFcltgA1qFGvVnzl6UJS0gQmYAf0AApxbGbpT9Fdx41xtKiop96
# eiL6SJUfq/tHI4D1nvi/a7dLl+LrdXga7Oo3mXkYS//WsyNodeav+vyL6wuA6mk7
# r/ww7QRMjt/fdW1jkT3RnVZOT7+AVyKheBEyIXrvQQqxP/uozKRdwaGIm1dxVk5I
# RcBCyZt2WwqASGv9eZ/BvW1taslScxMNelDNMYIVdDCCFXACAQEwgZUwfjELMAkG
# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z
# b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAd9r8C6Sp0q00AAAAAAB3zAN
# BglghkgBZQMEAgEFAKCBrjAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor
# BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgc5M7YXyG
# EXkLjvjS6U4ZENtMQdEwq4nxFkkM2OJ95fswQgYKKwYBBAGCNwIBDDE0MDKgFIAS
# AE0AaQBjAHIAbwBzAG8AZgB0oRqAGGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbTAN
# BgkqhkiG9w0BAQEFAASCAQB297kI0WDv9C81uRp2L5V48dQJgoHFz3aK9MNYIHCt
# Nf1TtAjKmH68ZKxJ5kpGA9trIaqehGMMqt/uK4XUqyNC7atov3KbxGvj4Y1lIBUj
# U3i0CgwfhQvmb5+dtidWxiJ1OWfwH2gKVPNnPg0b3q1XQTub7rxJkTBeo8JAr9fO
# tB8nbXI0Zcb+dpiGsVVjwsIu/39PAzIUcf+LpSM4wqdSUZoInSZ82gWDe1Nbx/5l
# D5ggC/f/DH0nKHyGTJA6s/6+zkj/Oag6aj+P1GbrknrVM+KvUVDctUlE5IxIuNDa
# hpdPBeUNUq3+79+WCO4fzgMfIPZjxTns31U4RoNcTtHDoYIS/jCCEvoGCisGAQQB
# gjcDAwExghLqMIIS5gYJKoZIhvcNAQcCoIIS1zCCEtMCAQMxDzANBglghkgBZQME
# AgEFADCCAVkGCyqGSIb3DQEJEAEEoIIBSASCAUQwggFAAgEBBgorBgEEAYRZCgMB
# MDEwDQYJYIZIAWUDBAIBBQAEIFJ4+xj9USrCsgmDUVT3tEyY0t/orVkw9oGYsJv2
# Xn4hAgZgsAUEWWUYEzIwMjEwNjA0MTIxOTMzLjY3NFowBIACAfSggdikgdUwgdIx
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1p
# Y3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEmMCQGA1UECxMdVGhh
# bGVzIFRTUyBFU046ODZERi00QkJDLTkzMzUxJTAjBgNVBAMTHE1pY3Jvc29mdCBU
# aW1lLVN0YW1wIFNlcnZpY2Wggg5NMIIE+TCCA+GgAwIBAgITMwAAAT7OyndSxfc0
# KwAAAAABPjANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMK
# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0
# IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0Eg
# MjAxMDAeFw0yMDEwMTUxNzI4MjVaFw0yMjAxMTIxNzI4MjVaMIHSMQswCQYDVQQG
# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQg
# SXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsTHVRoYWxlcyBUU1Mg
# RVNOOjg2REYtNEJCQy05MzM1MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFt
# cCBTZXJ2aWNlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvFTEyDzZ
# fpws404gSC0kt4VSyX/vaxwOfri89gQdxvfQNvvQARebKR3plqHz0ZHZW+bmFxyG
# tTh9zw20LSdpMcWYDFc1rzPuJvTNAnDkKyQP+TqrW7j/lDlCLbqi8ubo4EqSpkHr
# a0Zt15j2r/IJGZbu3QaRY6qYMZxxkkw4Y5ubAwV3E1p+TNzFg8nzgJ9kwEM4xvZA
# f9NhHhM2K/jx092xmKxyFfp0X0tboY9d1OyhdCXl8spOigE32g8zH12Y2NXTfI41
# 41LQU+9dKOKQ7YFF1kwofuGGwxMU0CsDimODWgr6VFVcNDd2tQbGubgdfLBGEBfj
# e0PyoOOXEO1m4QIDAQABo4IBGzCCARcwHQYDVR0OBBYEFJNa8534u9BiLWvwtbZU
# DraGiP17MB8GA1UdIwQYMBaAFNVjOlyKMZDzQ3t8RhvFM2hahW1VMFYGA1UdHwRP
# ME0wS6BJoEeGRWh0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1
# Y3RzL01pY1RpbVN0YVBDQV8yMDEwLTA3LTAxLmNybDBaBggrBgEFBQcBAQROMEww
# SgYIKwYBBQUHMAKGPmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMv
# TWljVGltU3RhUENBXzIwMTAtMDctMDEuY3J0MAwGA1UdEwEB/wQCMAAwEwYDVR0l
# BAwwCgYIKwYBBQUHAwgwDQYJKoZIhvcNAQELBQADggEBAKaz+RF9Wp+GkrkVj6cY
# 5djCdVepJFyufABJ1qKlCWXhOoYAcB7w7ZxzRC4Z2iY4bc9QU93sa2YDwhQwFPeq
# fKZfWSkmrcus49QB9EGPc9FwIgfBQK2AJthaYEysTawS40f6yc6w/ybotAclqFAr
# +BPDt0zGZoExvGc8ZpVAZpvSyXbzGLuKtm8K+R73VC4DUp4sRFck1Cx8ILvYdYSN
# YqORyh0Gwi3v4HWmw6HutafFOdFjaKQEcSsn0SNLfY25qOqnu6DL+NAo7z3qD0eB
# DISilWob5dllDcONfsu99UEtOnrbdl292yGNIyxilpI8XGNgGcZxKN6VqLBxAuKl
# WOYwggZxMIIEWaADAgECAgphCYEqAAAAAAACMA0GCSqGSIb3DQEBCwUAMIGIMQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNy
# b3NvZnQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxMDAeFw0xMDA3MDEy
# MTM2NTVaFw0yNTA3MDEyMTQ2NTVaMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX
# YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg
# Q29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAy
# MDEwMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqR0NvHcRijog7PwT
# l/X6f2mUa3RUENWlCgCChfvtfGhLLF/Fw+Vhwna3PmYrW/AVUycEMR9BGxqVHc4J
# E458YTBZsTBED/FgiIRUQwzXTbg4CLNC3ZOs1nMwVyaCo0UN0Or1R4HNvyRgMlhg
# RvJYR4YyhB50YWeRX4FUsc+TTJLBxKZd0WETbijGGvmGgLvfYfxGwScdJGcSchoh
# iq9LZIlQYrFd/XcfPfBXday9ikJNQFHRD5wGPmd/9WbAA5ZEfu/QS/1u5ZrKsajy
# eioKMfDaTgaRtogINeh4HLDpmc085y9Euqf03GS9pAHBIAmTeM38vMDJRF1eFpwB
# BU8iTQIDAQABo4IB5jCCAeIwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFNVj
# OlyKMZDzQ3t8RhvFM2hahW1VMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsG
# A1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNX2VsuP6KJc
# YmjRPZSQW9fOmhjEMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwubWljcm9z
# b2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIz
# LmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93d3cubWlj
# cm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3J0
# MIGgBgNVHSABAf8EgZUwgZIwgY8GCSsGAQQBgjcuAzCBgTA9BggrBgEFBQcCARYx
# aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL1BLSS9kb2NzL0NQUy9kZWZhdWx0Lmh0
# bTBABggrBgEFBQcCAjA0HjIgHQBMAGUAZwBhAGwAXwBQAG8AbABpAGMAeQBfAFMA
# dABhAHQAZQBtAGUAbgB0AC4gHTANBgkqhkiG9w0BAQsFAAOCAgEAB+aIUQ3ixuCY
# P4FxAz2do6Ehb7Prpsz1Mb7PBeKp/vpXbRkws8LFZslq3/Xn8Hi9x6ieJeP5vO1r
# VFcIK1GCRBL7uVOMzPRgEop2zEBAQZvcXBf/XPleFzWYJFZLdO9CEMivv3/Gf/I3
# fVo/HPKZeUqRUgCvOA8X9S95gWXZqbVr5MfO9sp6AG9LMEQkIjzP7QOllo9ZKby2
# /QThcJ8ySif9Va8v/rbljjO7Yl+a21dA6fHOmWaQjP9qYn/dxUoLkSbiOewZSnFj
# nXshbcOco6I8+n99lmqQeKZt0uGc+R38ONiU9MalCpaGpL2eGq4EQoO4tYCbIjgg
# tSXlZOz39L9+Y1klD3ouOVd2onGqBooPiRa6YacRy5rYDkeagMXQzafQ732D8OE7
# cQnfXXSYIghh2rBQHm+98eEA3+cxB6STOvdlR3jo+KhIq/fecn5ha293qYHLpwms
# ObvsxsvYgrRyzR30uIUBHoD7G4kqVDmyW9rIDVWZeodzOwjmmC3qjeAzLhIp9cAv
# VCch98isTtoouLGp25ayp0Kiyc8ZQU3ghvkqmqMRZjDTu3QyS99je/WZii8bxyGv
# WbWu3EQ8l1Bx16HSxVXjad5XwdHeMMD9zOZN+w2/XU/pnR4ZOC+8z1gFLu8NoFA1
# 2u8JJxzVs341Hgi62jbb01+P3nSISRKhggLXMIICQAIBATCCAQChgdikgdUwgdIx
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1p
# Y3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEmMCQGA1UECxMdVGhh
# bGVzIFRTUyBFU046ODZERi00QkJDLTkzMzUxJTAjBgNVBAMTHE1pY3Jvc29mdCBU
# aW1lLVN0YW1wIFNlcnZpY2WiIwoBATAHBgUrDgMCGgMVAKBMFej0xjCTjCk1sTdT
# Ka+TzJDUoIGDMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0
# b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3Jh
# dGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwDQYJ
# KoZIhvcNAQEFBQACBQDkZGZdMCIYDzIwMjEwNjA0MTY0NDEzWhgPMjAyMTA2MDUx
# NjQ0MTNaMHcwPQYKKwYBBAGEWQoEATEvMC0wCgIFAORkZl0CAQAwCgIBAAICBfoC
# Af8wBwIBAAICEjYwCgIFAORlt90CAQAwNgYKKwYBBAGEWQoEAjEoMCYwDAYKKwYB
# BAGEWQoDAqAKMAgCAQACAwehIKEKMAgCAQACAwGGoDANBgkqhkiG9w0BAQUFAAOB
# gQB2bU/ewagl+X+M7k5gRaD/7aOT5Anl0RsidL9rJRu11vC7/upA4Y4/9J4B3L0N
# GIVORMOlphwpqhD5neOKgdipcUV8tDAmgDpoZG06J0lxvH8hIWxOVf2l3Ck9ny7O
# f3BaCSJqUQN/SYvSuXJiT/LXQ0Wmxdab6x72e65bcgHrGjGCAw0wggMJAgEBMIGT
# MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMT
# HU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAABPs7Kd1LF9zQrAAAA
# AAE+MA0GCWCGSAFlAwQCAQUAoIIBSjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQ
# AQQwLwYJKoZIhvcNAQkEMSIEIKOfu1Re1t9AUBrD/kQWRO46XukdQRQLPyi1jG14
# N+g7MIH6BgsqhkiG9w0BCRACLzGB6jCB5zCB5DCBvQQgi+vOjaqNTvKOZGut49HX
# rqtwUj2ZCnVOurBwfgQxmxMwgZgwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UE
# CBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9z
# b2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQ
# Q0EgMjAxMAITMwAAAT7OyndSxfc0KwAAAAABPjAiBCDYkpEalCYymZe8HTn8Dgz9
# 2JeTP6m3LuT76rFnGZaO8jANBgkqhkiG9w0BAQsFAASCAQCHZLS756Oji+OlPMFm
# JcxzHjA0h3sniVqulEM9o9i6G5xCQwoXOhQPDphxwtDsTneoU6EGAOtIedU64xz2
# YpOimmIUMq3+K39FDF5AbSGqAtpni7lVRVLQwC/GnUJb+JpmLK6ueSWgGe+TS2kw
# /Lpe5YWszEToWYpSGshnc3OhYy4b/V6UWyC7zaH3tBn9BhpQzJKl2po1IW/Mf2eY
# zHcMyyVgLl2xIU+nMH3yLEzeBPQsGLHeFTeI0t62N13zDmsU7a6mgZP+uelAyjJ4
# s4SymqQxOCZn3Y15EOMldP/WmuxHr2UcKeHqabVFehQWN+vDDs91KtajTiqzDgQ5
# kZS+
# SIG # End signature block