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-AzureURIs
{
    <#
    .SYNOPSIS
        Resolve Azure URIs for a given Azure Service
    .DESCRIPTION
        Resolve Azure URIs for a given Azure Service
    .EXAMPLE
        Get-AzureURIs -AzureEnvironment AzureCloud
    .INPUTS
        AzureEnvironment - string - should be AzureCloud, AzureChinaCloud, AzureGermanCloud
    .OUTPUTS
        None - logging only
    .NOTES
        General notes
    #>

    [OutputType([Hashtable])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateSet('AzureCloud', 'AzureChinaCloud', 'AzureUSGovernment', 'AzureGermanCloud')]
        [string] $AzureEnvironment
    )

    $data = switch ($AzureEnvironment)
    {
        'AzureCloud'
        {
            @{
                GraphUri             = "https://graph.windows.net/"
                LoginUri             = "https://login.microsoftonline.com/"
                ManagementServiceUri = "https://management.core.windows.net/"
                ARMUri               = "https://management.azure.com/"
            }
        }

        'AzureChinaCloud'
        {
            @{
                GraphUri             = "https://graph.chinacloudapi.cn/"
                LoginUri             = "https://login.chinacloudapi.cn/"
                ManagementServiceUri = "https://management.core.chinacloudapi.cn/"
                ARMUri               = "https://management.chinacloudapi.cn/"
            }
        }

        'AzureUSGovernment'
        {
            @{
                GraphUri             = "https://graph.windows.net/"
                LoginUri             = "https://login-us.microsoftonline.com/"
                ManagementServiceUri = "https://management.core.usgovcloudapi.net/"
                ARMUri               = "https://management.usgovcloudapi.net/"
            }
        }

        'AzureGermanCloud'
        {
            @{
                GraphUri             = "https://graph.cloudapi.de/"
                LoginUri             = "https://login.microsoftonline.de/"
                ManagementServiceUri = "https://management.core.cloudapi.de/"
                ARMUri               = "https://management.microsoftazure.de/"
            }
        }

        default
        {
            throw New-Object NotImplementedException("Unknown environment type '$AzureEnvironment'")
        }
    }

    return $data
}

function Get-AADToken
{
    [CmdletBinding()]
    param(
        [string]
        $ResourceUri,

        [string]
        $TenantId,

        [PSCredential]
        $Credential,

        [string]
        $UserId,

        [Hashtable]
        $AzureURIs,

        [Microsoft.IdentityModel.Clients.ActiveDirectory.PromptBehavior]
        $PromptBehavior
    )

    $context = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext "$($AzureURIs.LoginUri)$TenantId"

    if ($Credential -ne $null)
    {
        $aadCredential = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.UserCredential $Credential.UserName,$Credential.Password
        return $context.AcquireToken($ResourceUri, $AzurePowerShellClientID, $aadCredential)
    }
    else
    {
        if ([string]::IsNullOrEmpty($UserId))
        {
            $userIdentifier = [Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier]::AnyUser
        }
        else
        {
            $userIdentifier = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier $UserId, OptionalDisplayableId
        }

        return $context.AcquireToken(
                    $ResourceUri, $AzurePowerShellClientID, $AzurePowerShellReturnUri,
                    $PromptBehavior, $userIdentifier, "site_id=501358&display=popup")
    }
}

function Set-CookiesOptions
{
    [CmdletBinding(SupportsShouldProcess=$True,ConfirmImpact="Medium")]
    $signature = @'
    [DllImport("wininet.dll", SetLastError = true)]
    public static extern bool InternetSetOption(IntPtr hInternet, int dwOption, IntPtr lpBuffer, int lpdwBufferLength);
 
    [DllImport("wininet.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    public static extern int PrivacySetZonePreference(int dwZone, int dwType, int dwTemplate, string pszPreference);
'@


    $type = Add-Type -MemberDefinition $signature -Name 'NativeMethods' -Namespace 'AzureStack.SetCookies' -PassThru -ErrorAction Ignore

    $INTERNET_OPTION_END_BROWSER_SESSION = 42
    $URLZONE_INTERNET = 3
    $PRIVACY_TYPE_FIRST_PARTY = 0
    $PRIVACY_TEMPLATE_ADVANCED = 101
    $preference = "IE6-P3PV1/settings: always=a session=a"

    $type::InternetSetOption([IntPtr]::Zero, $INTERNET_OPTION_END_BROWSER_SESSION, [IntPtr]::Zero, 0)
    $type::PrivacySetZonePreference($URLZONE_INTERNET, $PRIVACY_TYPE_FIRST_PARTY, $PRIVACY_TEMPLATE_ADVANCED, $preference)
}

function Get-AADTenantIds
{
    [OutputType([Hashtable])]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [Hashtable] $AzureURIs,

        [Parameter(Mandatory=$false)]
        [PSCredential] $Credential
    )

    Set-CookiesOptions

    $token = Get-AADToken -ResourceUri $AzureURIs.ManagementServiceUri -TenantId "Common" -Credential $Credential -PromptBehavior Always -AzureURIs $AzureURIs

    $UserInfo = @{
        FirstName = $token.UserInfo.GivenName
        LastName = $token.UserInfo.FamilyName
        Account = $token.UserInfo.DisplayableId
    }

    $tenantsResponse = Invoke-RestMethod -Method Get -Uri "$($AzureURIs.ARMUri)tenants?api-version=2016-02-01" -Headers @{Authorization = "Bearer $($token.AccessToken)"}
    $tenantIds = [Array]$tenantsResponse.Value.tenantId

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

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

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

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

        [Parameter(Mandatory=$false)]
        [PSCredential]
        $Credential
    )

    try
    {
        $token = Get-AADToken -ResourceUri $AzureURIs.GraphUri -TenantId $TenantId -Credential $Credential -PromptBehavior never -UserId $UserId -AzureURIs $AzureURIs
    }
    catch [Microsoft.IdentityModel.Clients.ActiveDirectory.AdalException]
    {
        if ($_.Exception.ErrorCode -eq "user_interaction_required")
        {
            $token = Get-AADToken -ResourceUri $AzureURIs.GraphUri -TenantId $TenantId -Credential $Credential -PromptBehavior auto -UserId $UserId -AzureURIs $AzureURIs
        }
        else
        {
            throw
        }
    }

    $graphUri = "$($AzureURIs.GraphUri)myOrganization/oauth2PermissionGrants?`$top=1&api-version=1.5"

    try
    {
        Invoke-RestMethod -Method Get -Uri $graphUri -Headers @{Authorization = "Bearer $($token.AccessToken)"} | Out-Null
    }
    catch
    {
        if ($_.Exception.Response.StatusCode -eq 'Internal Server Error')
        {
            throw ($LocalizedData.GraphEndpointError -f $graphUri)
        }
        if ($_.Exception.Response.StatusCode -eq 'Forbidden')
        {
            return $null
        }
        else
        {
            throw $_
        }
    }

    $tenantResponse = Invoke-RestMethod -Method Get -Uri "$($AzureURIs.GraphUri)myOrganization/tenantDetails?api-version=1.5" -Headers @{Authorization = "Bearer $($token.AccessToken)"}

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

# Dlls packaged with AzureRM
$AzureRMBase = (Get-Module AzureRM.Profile -ListAvailable | Sort-Object version -Descending | Select-Object -first 1).ModuleBase
[System.Reflection.Assembly]::LoadFile("$AzureRMBase\Microsoft.IdentityModel.Clients.ActiveDirectory.dll") | Out-Null
[System.Reflection.Assembly]::LoadFile("$AzureRMBase\Microsoft.IdentityModel.Clients.ActiveDirectory.WindowsForms.dll") | Out-Null

function Get-AADTenantDetails
{
    <#
    .SYNOPSIS
        Resolve Azure Tenant Details
    .DESCRIPTION
        Resolve Azure Tenant Details
    .EXAMPLE
        Get-AADTenantDetails -AzureEnvironment AzureCloud -Credential $tenantCredential
    .INPUTS
        AzureEnvironment - string - should be AzureCloud, AzureChinaCloud, AzureGermanCloud
        Credetential - PSCredential
    .OUTPUTS
        Hashtable of tenant details
    .NOTES
        General notes
    #>

    [OutputType([Hashtable])]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string] $AzureEnvironment,

        [Parameter(Mandatory=$false)]
        [PSCredential] $Credential,

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

    Write-Progress -Activity $LocalizedData.InfoSigningIn
    $azureURIs = Get-AzureURIs -AzureEnvironment $AzureEnvironment

    $TenantsInfo = Get-AADTenantIds -Credential $Credential -AzureURIs $azureURIs
    $TenantIds = [Array] $TenantsInfo.TenantIds
    
    # Workaround to add the target tenant Id if specified to the list of "resolved" tenant memberships
    if ($AADDirectoryTenantName)
    {
        $targetTenantId = Get-TenantIdFromName -azureEnvironment $AzureEnvironment -tenantName $AADDirectoryTenantName
        if ($TenantIds -inotcontains $targetTenantId)
        {
            $TenantIds = ([Array] $TenantsInfo.TenantIds) + $targetTenantId
        }
    }

    $tenantDetails = @()
    for ($i = 1; $i -le $TenantIds.Count; $i++)
    {
        Write-Progress -Activity $LocalizedData.InfoSigningIn -PercentComplete ($i * 100 / ($TenantIds.Count + 1))
        $tenantDetail =  Get-AADTenantDetail -Credential $Credential -TenantId $TenantIds[$i-1] -UserId $TenantsInfo.UserInfo.Account -AzureURIs $azureURIs
        if ($tenantDetail)
        {
            $tenantDetails += $tenantDetail
        }
    }

    Write-Progress -Activity $LocalizedData.InfoSigningIn -Completed

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

function Get-AADUserObjectId
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string] $AzureEnvironment,

        [string] $TenantId,
        [PSCredential] $Credential
    )

    $azureURIs = Get-AzureURIs -AzureEnvironment $AzureEnvironment

    return (Get-AADToken -ResourceUri $azureURIs.GraphUri -TenantId $TenantId -Credential $Credential -AzureURIs $azureURIs).UserInfo.UniqueId
}

<#
.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 -AzureEnvironment AzureCloud -AADAdminCredential $AADAdminCredential $AADDirectoryTenantName $AADDirectoryTenantName
    .INPUTS
        AzureEnvironment - string - should be AzureCloud, AzureChinaCloud, AzureGermanCloud
        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=$true)]
        [ValidateNotNullOrEmpty()]
        [string] $AzureEnvironment,

        [Parameter(Mandatory=$false)]
        [PSCredential] $AADAdminCredential,

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

    $claimsProvider = $AzureEnvironment

    $azureResult = Get-AADTenantDetails -AzureEnvironment $AzureEnvironment -Credential $AADAdminCredential -AADDirectoryTenantName $AADDirectoryTenantName
    $azureToken = $null
    $azureRefreshToken = $null
    $tenantId = $null
    $tenantName = $null

    if ($null -eq $azureResult.TenantDetails -or @($azureResult.TenantDetails).Count -eq 0)
    {
        Write-Error ($LocalizedData.AADAccountNotAdmin -f $($azureResult.UserInfo.Account))
    }
    elseif (@($azureResult.TenantDetails).Count -gt 1 -and -not $AADDirectoryTenantName)
    {
        Write-Error ($LocalizedData.MoreThanOneTenant -f @($($azureResult.UserInfo.Account), $($azureResult.TenantDetails.DomainName -join ', ')))
    }
    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-Error ($LocalizedData.NotAdminOfTenant -f @($($azureResult.UserInfo.Account), $AADDirectoryTenantName, $($azureResult.TenantDetails.DomainName -join ', ')))
        }
    }
    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] $azureEnvironment,

        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        [string] $tenantName
    )

    $azureURIs = Get-AzureURIs -AzureEnvironment $AzureEnvironment

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

    $response = Invoke-RestMethod -Uri $uri -Method Get

    Write-Verbose -Message "using token_endpoint $($response.token_endpoint) to parse tenant id"
    $tenantId = $response.token_endpoint.Split('/')[3]
 
    $tenantIdGuid = [guid]::NewGuid()
    $result = [guid]::TryParse($tenantId, [ref] $tenantIdGuid)

    if(-not $result)
    {
        Write-Error "Error obtaining tenant id from tenant name"
    }
    else
    {
        Write-Verbose -Message "Tenant Name: $tenantName Tenant id: $tenantId"
        return $tenantId
    }
}


Export-ModuleMember -Function Get-AzureADTenantDetails
Export-ModuleMember -Function Get-AzureURIs
Export-ModuleMember -Function Get-TenantIdFromName
Export-ModuleMember -Function Get-AADToken

# SIG # Begin signature block
# MIIkhwYJKoZIhvcNAQcCoIIkeDCCJHQCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDNXMW3a0uE9Y3A
# R7JCGq7en00CE5+Zgs+v8UvTQyq6EKCCDYEwggX/MIID56ADAgECAhMzAAABA14l
# HJkfox64AAAAAAEDMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMTgwNzEyMjAwODQ4WhcNMTkwNzI2MjAwODQ4WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQDRlHY25oarNv5p+UZ8i4hQy5Bwf7BVqSQdfjnnBZ8PrHuXss5zCvvUmyRcFrU5
# 3Rt+M2wR/Dsm85iqXVNrqsPsE7jS789Xf8xly69NLjKxVitONAeJ/mkhvT5E+94S
# nYW/fHaGfXKxdpth5opkTEbOttU6jHeTd2chnLZaBl5HhvU80QnKDT3NsumhUHjR
# hIjiATwi/K+WCMxdmcDt66VamJL1yEBOanOv3uN0etNfRpe84mcod5mswQ4xFo8A
# DwH+S15UD8rEZT8K46NG2/YsAzoZvmgFFpzmfzS/p4eNZTkmyWPU78XdvSX+/Sj0
# NIZ5rCrVXzCRO+QUauuxygQjAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUR77Ay+GmP/1l1jjyA123r3f3QP8w
# UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1
# ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDM3OTY1MB8GA1UdIwQYMBaAFEhu
# ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu
# bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w
# Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3
# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx
# MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAn/XJ
# Uw0/DSbsokTYDdGfY5YGSz8eXMUzo6TDbK8fwAG662XsnjMQD6esW9S9kGEX5zHn
# wya0rPUn00iThoj+EjWRZCLRay07qCwVlCnSN5bmNf8MzsgGFhaeJLHiOfluDnjY
# DBu2KWAndjQkm925l3XLATutghIWIoCJFYS7mFAgsBcmhkmvzn1FFUM0ls+BXBgs
# 1JPyZ6vic8g9o838Mh5gHOmwGzD7LLsHLpaEk0UoVFzNlv2g24HYtjDKQ7HzSMCy
# RhxdXnYqWJ/U7vL0+khMtWGLsIxB6aq4nZD0/2pCD7k+6Q7slPyNgLt44yOneFuy
# bR/5WcF9ttE5yXnggxxgCto9sNHtNr9FB+kbNm7lPTsFA6fUpyUSj+Z2oxOzRVpD
# MYLa2ISuubAfdfX2HX1RETcn6LU1hHH3V6qu+olxyZjSnlpkdr6Mw30VapHxFPTy
# 2TUxuNty+rR1yIibar+YRcdmstf/zpKQdeTr5obSyBvbJ8BblW9Jb1hdaSreU0v4
# 6Mp79mwV+QMZDxGFqk+av6pX3WDG9XEg9FGomsrp0es0Rz11+iLsVT9qGTlrEOla
# P470I3gwsvKmOMs1jaqYWSRAuDpnpAdfoP7YO0kT+wzh7Qttg1DO8H8+4NkI6Iwh
# SkHC3uuOW+4Dwx1ubuZUNWZncnwa6lL2IsRyP64wggd6MIIFYqADAgECAgphDpDS
# 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/BvW1taslScxMNelDNMYIWXDCCFlgCAQEwgZUwfjELMAkG
# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z
# b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAQNeJRyZH6MeuAAAAAABAzAN
# BglghkgBZQMEAgEFAKCB3jAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor
# BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgJZqGOBUy
# E/vQZQroo5lNJtt/MdJR2fPH8yd82IE21BgwcgYKKwYBBAGCNwIBDDFkMGKgSIBG
# AE0AaQBjAHIAbwBzAG8AZgB0ACAAQQB6AHUAcgBlAFMAdABhAGMAawAgAFAAYQBy
# AHQAbgBlAHIAVABvAG8AbABrAGkAdKEWgBRodHRwOi8vQ29kZVNpZ25JbmZvIDAN
# BgkqhkiG9w0BAQEFAASCAQCUYx/BxK7i/k0XFWXwYeK2u4u18y4Sq8aZNzwMaWF0
# 45TWgyRfYEifhjA9zxNoBQZXIsvz8bRK+2J8Bd5Li5skjtVn+89T8TKAXgPEIMSJ
# zwOXDh1GzkOyCnxW+7ivhjlsUWqTTjt6y+zN8OYxskFyhX2yjIoqujCSI/39xiSX
# WRv/Iw3RkjPofd/+OqUyrDgKnL1LM0Hz6gYFvxn8+JqLyIO4qvt0qvSLdAjyALHS
# tplLPnzLa/uP245IrNq6jTh2SIwuMvXVu3U60MLA3zVlxUCV2HHuaTyNUxs5DyLy
# jDjLB8h9BVn5MF7wmbzZGEljl2HhCqEuJjBABLV+xiYXoYITtjCCE7IGCisGAQQB
# gjcDAwExghOiMIITngYJKoZIhvcNAQcCoIITjzCCE4sCAQMxDzANBglghkgBZQME
# AgEFADCCAVUGCyqGSIb3DQEJEAEEoIIBRASCAUAwggE8AgEBBgorBgEEAYRZCgMB
# MDEwDQYJYIZIAWUDBAIBBQAEIML/FS1ZpYnlql6NAGquSoZbjE6b+BhxCwQGDoxj
# yjDfAgZb2cjKK30YEzIwMTgxMTAxMjE1MzQyLjA0NVowBIACAfSggdSkgdEwgc4x
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1p
# Y3Jvc29mdCBPcGVyYXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMg
# VFNTIEVTTjoxNDhDLUM0QjktMjA2NjElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUt
# U3RhbXAgU2VydmljZaCCDyEwggT1MIID3aADAgECAhMzAAAA1acj5XiVagn/AAAA
# AADVMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo
# aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y
# cG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEw
# MB4XDTE4MDgyMzIwMjY0NVoXDTE5MTEyMzIwMjY0NVowgc4xCzAJBgNVBAYTAlVT
# MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVy
# YXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjoxNDhD
# LUM0QjktMjA2NjElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vydmlj
# ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMIUCSuuF/E0gjL87onw
# du50M1BwUxQwtrZWEQYCmLMhGhNR/3eQxBD9lqY4FYxhRZ1bxfsIjs7SKvcF0m1Y
# fhgK5j9BdZvzueedkEY8MVUu41zB18dn+Nmaelz5vFz9BLq5hJPAEdZg3ZR+4dP7
# 19tVXJP/iVkslLTTKH/YqT8VciemXOUaCsK/Re7xdBe8qBShsOwbgD7xVYwgmevz
# +bSZ3UsWClccBB9kMhNHhniIKkuMo4BNJ5te+yp8KBVyzontKmtOhS1XKNSIywEW
# AzRGKZtlP36DVGOy9SGajweV0RjHkqZMUD1tEiIaDoBpiZXacHPne1FQGwSjK4NM
# lEsCAwEAAaOCARswggEXMB0GA1UdDgQWBBQpaKMxlHZD2V9dDZuf07KE9mI3hDAf
# BgNVHSMEGDAWgBTVYzpcijGQ80N7fEYbxTNoWoVtVTBWBgNVHR8ETzBNMEugSaBH
# hkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNU
# aW1TdGFQQ0FfMjAxMC0wNy0wMS5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUF
# BzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1RpbVN0
# YVBDQV8yMDEwLTA3LTAxLmNydDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsG
# AQUFBwMIMA0GCSqGSIb3DQEBCwUAA4IBAQB52cvC6cTRwKstDtImv9pnoAv99sC2
# wQLy4rmBzWL2kI/sleBNXo74cqdAWoYfLRCQo+DuW2m5/0mMe4DpvPDLYCQSSJkA
# tu/hUL80wik5ZqaNvIfp7h1/CJ/KMuIgd8WbhNZxLyG5S4W7As667QNifweCrjFq
# ZMV9XtBO2wjz9Qti1RjdOImhX7V4kdCYKStdQg2xJhUU9ZtPqGprthaPqto9Fheq
# s37Cw2vO3tAjYqMXTZCvybz+4bUHxIWvqvJIXLpuPPz1zCvCotLfBzzBiV9miIEB
# Xsnv2qIIkdDnISqlrIZ1nXdRHdXl4ZhBf3wAZR/EwILvKaksKFYzrk4LMIIGcTCC
# BFmgAwIBAgIKYQmBKgAAAAAAAjANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV
# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJv
# b3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMTAwNzAxMjEzNjU1WhcN
# MjUwNzAxMjE0NjU1WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv
# bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0
# aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCASIw
# DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKkdDbx3EYo6IOz8E5f1+n9plGt0
# VBDVpQoAgoX77XxoSyxfxcPlYcJ2tz5mK1vwFVMnBDEfQRsalR3OCROOfGEwWbEw
# RA/xYIiEVEMM1024OAizQt2TrNZzMFcmgqNFDdDq9UeBzb8kYDJYYEbyWEeGMoQe
# dGFnkV+BVLHPk0ySwcSmXdFhE24oxhr5hoC732H8RsEnHSRnEnIaIYqvS2SJUGKx
# Xf13Hz3wV3WsvYpCTUBR0Q+cBj5nf/VmwAOWRH7v0Ev9buWayrGo8noqCjHw2k4G
# kbaICDXoeByw6ZnNPOcvRLqn9NxkvaQBwSAJk3jN/LzAyURdXhacAQVPIk0CAwEA
# AaOCAeYwggHiMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBTVYzpcijGQ80N7
# fEYbxTNoWoVtVTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMC
# AYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvX
# zpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20v
# cGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYI
# KwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDCBoAYDVR0g
# AQH/BIGVMIGSMIGPBgkrBgEEAYI3LgMwgYEwPQYIKwYBBQUHAgEWMWh0dHA6Ly93
# d3cubWljcm9zb2Z0LmNvbS9QS0kvZG9jcy9DUFMvZGVmYXVsdC5odG0wQAYIKwYB
# BQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AUABvAGwAaQBjAHkAXwBTAHQAYQB0AGUA
# bQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAAfmiFEN4sbgmD+BcQM9naOh
# IW+z66bM9TG+zwXiqf76V20ZMLPCxWbJat/15/B4vceoniXj+bzta1RXCCtRgkQS
# +7lTjMz0YBKKdsxAQEGb3FwX/1z5Xhc1mCRWS3TvQhDIr79/xn/yN31aPxzymXlK
# kVIArzgPF/UveYFl2am1a+THzvbKegBvSzBEJCI8z+0DpZaPWSm8tv0E4XCfMkon
# /VWvL/625Y4zu2JfmttXQOnxzplmkIz/amJ/3cVKC5Em4jnsGUpxY517IW3DnKOi
# PPp/fZZqkHimbdLhnPkd/DjYlPTGpQqWhqS9nhquBEKDuLWAmyI4ILUl5WTs9/S/
# fmNZJQ96LjlXdqJxqgaKD4kWumGnEcua2A5HmoDF0M2n0O99g/DhO3EJ3110mCII
# YdqwUB5vvfHhAN/nMQekkzr3ZUd46PioSKv33nJ+YWtvd6mBy6cJrDm77MbL2IK0
# cs0d9LiFAR6A+xuJKlQ5slvayA1VmXqHczsI5pgt6o3gMy4SKfXAL1QnIffIrE7a
# KLixqduWsqdCosnPGUFN4Ib5KpqjEWYw07t0MkvfY3v1mYovG8chr1m1rtxEPJdQ
# cdeh0sVV42neV8HR3jDA/czmTfsNv11P6Z0eGTgvvM9YBS7vDaBQNdrvCScc1bN+
# NR4Iuto229Nfj950iEkSoYIDrzCCApcCAQEwgf6hgdSkgdEwgc4xCzAJBgNVBAYT
# AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1pY3Jvc29mdCBP
# cGVyYXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjox
# NDhDLUM0QjktMjA2NjElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vy
# dmljZaIlCgEBMAkGBSsOAwIaBQADFQCtwyS80dl18F3Q3UUfOTGebtaesaCB3jCB
# 26SB2DCB1TELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNV
# BAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEpMCcG
# A1UECxMgTWljcm9zb2Z0IE9wZXJhdGlvbnMgUHVlcnRvIFJpY28xJzAlBgNVBAsT
# Hm5DaXBoZXIgTlRTIEVTTjo1N0Y2LUMxRTAtNTU0QzErMCkGA1UEAxMiTWljcm9z
# b2Z0IFRpbWUgU291cmNlIE1hc3RlciBDbG9jazANBgkqhkiG9w0BAQUFAAIFAN+F
# rwYwIhgPMjAxODExMDIwMDU3NDJaGA8yMDE4MTEwMzAwNTc0MlowdjA8BgorBgEE
# AYRZCgQBMS4wLDAKAgUA34WvBgIBADAJAgEAAgEhAgH/MAcCAQACAhj2MAoCBQDf
# hwCGAgEAMDYGCisGAQQBhFkKBAIxKDAmMAwGCisGAQQBhFkKAwGgCjAIAgEAAgMH
# oSChCjAIAgEAAgMHoSAwDQYJKoZIhvcNAQEFBQADggEBALwozOEZ9M6iJsdNsfFg
# d5wm4k2VjObqEv39lkVwILtJ+pK7tvbCY1uR36qK74C02PD1n6jICtppnJQczj8o
# 3hQVVbQJ6+ESOfjlcCAjy+4zc5B57rkxplenv4MwhraxU6bZOUB8ScPtSB6i1n+j
# biSHadBZT+e7eloeNjyEloz2J3tOGTmU7rvpv/WqSYL8zxJFZJsIJJflKzzsA/7a
# ziGqfa6ONKirq9Qc2vH+5yMEhCo8xVXtOMhsHwGZtHEQn9214Q8c8JytC5L+gGeg
# lE+gXRbCvXfnMLbMk8a/l1BuQrFIHzMDSWas/7rlk3zt/DL1J6Lp0iLG/g0V5uXO
# vqUxggL1MIIC8QIBATCBkzB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu
# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
# cmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAIT
# MwAAANWnI+V4lWoJ/wAAAAAA1TANBglghkgBZQMEAgEFAKCCATIwGgYJKoZIhvcN
# AQkDMQ0GCyqGSIb3DQEJEAEEMC8GCSqGSIb3DQEJBDEiBCB4MwMSHgLCaW5RAxFa
# JM11svQxlpSvAwtFo+9rGizh2TCB4gYLKoZIhvcNAQkQAgwxgdIwgc8wgcwwgbEE
# FK3DJLzR2XXwXdDdRR85MZ5u1p6xMIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzAR
# BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1p
# Y3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3Rh
# bXAgUENBIDIwMTACEzMAAADVpyPleJVqCf8AAAAAANUwFgQUDcD7Gv0/x9wC/Ohl
# 7maZLF2INvAwDQYJKoZIhvcNAQELBQAEggEAZqf+C2NME6jEjy1rjrKzlhvDJIqn
# A/3czTxCEX9qxuaTXPyRgB4/V5BVElrFsFLOOqw/d9mtYr2djDO/rlu3fhE140Rx
# 1gvqWxywCZ0CHnxmVVX1pG4nhqUj7jpySrBsFCmQbe4VaTxw19qQmR43HFc8cwYB
# mMy5V4nEugd71nzmjNm9NF3IfM4hU9cvBA4tVZGmOl6nw5GE2ZYP1ii1DY0i85Q8
# sZSxVzXbWT6LNTVluEo9DP6wl91V7tVns9VURo0H0zwdQkcmQ6oLJNMWHlFqVNyg
# dTYJ6FpVOLccPcgFdXbTCn8yj2UajeUrlTZ7JKPKvNuv5nBPhF5HATLwMg==
# SIG # End signature block