Identity/AzureStack.Identity.psm1

# Copyright (c) Microsoft Corporation. All rights reserved.
# See LICENSE.txt in the project root for license information.

<#
.Synopsis
    Get the Guid of the directory tenant
.DESCRIPTION
    This function fetches the OpenID configuration metadata from the identity system and parses the Directory TenantID out of it.
    Azure Stack AD FS is configured to be a single tenanted identity system with a TenantID.
.EXAMPLE
    Get-AzsDirectoryTenantIdentifier -authority https://login.windows.net/microsoft.onmicrosoft.com
.EXAMPLE
    Get-AzsDirectoryTenantIdentifier -authority https://adfs.local.azurestack.external/adfs
#>


function Get-AzsDirectoryTenantidentifier {
    [CmdletBinding()]
    Param
    (
        # The Authority of the identity system, e.g. "https://login.windows.net/microsoft.onmicrosoft.com"
        [Parameter(Mandatory = $true,
            Position = 0)]
        $Authority
    )

    return $(Invoke-RestMethod $("{0}/.well-known/openid-configuration" -f $authority.TrimEnd('/'))).issuer.TrimEnd('/').Split('/')[-1]
}

<#
   .Synopsis
      This function is used to create a Service Principal on the AD Graph in an AD FS topology
   .DESCRIPTION
      The command creates a certificate in the cert store of the local user and uses that certificate to create a Service Principal in the Azure Stack Stamp Active Directory.
   .EXAMPLE
      $servicePrincipal = New-AzsAdGraphServicePrincipal -DisplayName "myapp12" -AdminCredential $(Get-Credential) -Verbose
#>


function New-AzsAdGraphServicePrincipal {
    [CmdletBinding()]
    Param
    (
        # Display Name of the Service Principal
        [ValidatePattern("[a-zA-Z0-9-]{3,}")]
        [Parameter(Mandatory = $true,
            Position = 0)]
        $DisplayName,

        # PEP Machine name
        [string]
        $ERCSMachineName = "Azs-ERCS01",

        # Domain Administrator Credential to create Service Principal
        [Parameter(Mandatory = $true,
            Position = 2)]
        [System.Management.Automation.PSCredential]
        $AdminCredential
    )
    $ApplicationGroupName = $DisplayName
    $computerName = $ERCSMachineName
    $cloudAdminCredential = $AdminCredential
    $domainAdminSession = New-PSSession -ComputerName $computerName  -Credential $cloudAdminCredential -configurationname privilegedendpoint  -Verbose
    $GraphClientCertificate = New-SelfSignedCertificate -CertStoreLocation "cert:\CurrentUser\My" -Subject "CN=$ApplicationGroupName" -KeySpec KeyExchange
    $graphRedirectUri = "https://localhost/".ToLowerInvariant()
    $ApplicationName = $ApplicationGroupName
    $application = Invoke-Command -Session $domainAdminSession -Verbose -ErrorAction Stop  `
        -ScriptBlock { New-GraphApplication -Name $using:ApplicationName  -ClientRedirectUris $using:graphRedirectUri -ClientCertificates $using:GraphClientCertificate }
    
    return $application
}

# Exposed Functions

<#
.Synopsis
Adds a Guest Directory Tenant to Azure Stack.
.DESCRIPTION
Running this cmdlet will add the specified directory tenant to the Azure Stack whitelist.
.EXAMPLE
$adminARMEndpoint = "https://adminmanagement.local.azurestack.external"
$azureStackDirectoryTenant = "<homeDirectoryTenant>.onmicrosoft.com"
$guestDirectoryTenantToBeOnboarded = "<guestDirectoryTenant>.onmicrosoft.com"
 
Register-AzsGuestDirectoryTenant -AdminResourceManagerEndpoint $adminARMEndpoint -DirectoryTenantName $azureStackDirectoryTenant -GuestDirectoryTenantName $guestDirectoryTenantToBeOnboarded
#>


function Register-AzsGuestDirectoryTenant {
    [CmdletBinding()]
    param
    (
        # The endpoint of the Azure Stack Resource Manager service.
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [ValidateScript( { $_.Scheme -eq [System.Uri]::UriSchemeHttps })]
        [uri] $AdminResourceManagerEndpoint,

        # The name of the home Directory Tenant in which the Azure Stack Administrator subscription resides.
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $DirectoryTenantName,

        # The names of the guest Directory Tenants which are to be onboarded.
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string[]] $GuestDirectoryTenantName,

        # The location of your Azure Stack deployment.
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $Location,

        # The identifier of the Administrator Subscription. If not specified, the script will attempt to use the set default subscription.
        [ValidateNotNull()]
        [string] $SubscriptionId = $null,

        # The display name of the Administrator Subscription. If not specified, the script will attempt to use the set default subscription.
        [ValidateNotNull()]
        [string] $SubscriptionName = $null,

        # The name of the resource group in which the directory tenant registration resource should be created (resource group must already exist).
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $ResourceGroupName = $null,

        # Optional: A credential used to authenticate with Azure Stack. Must support a non-interactive authentication flow. If not provided, the script will prompt for user credentials.
        [Parameter()]
        [ValidateNotNull()]
        [pscredential] $AutomationCredential = $null
    )

    $ErrorActionPreference = 'Stop'
    $VerbosePreference = 'Continue'

    Import-Module 'AzureRm.Profile' -Verbose:$false 4> $null

    function Invoke-Main {
        # Initialize the Azure PowerShell module to communicate with Azure Stack. Will prompt user for credentials.
        $azureEnvironment = Initialize-AzureRmEnvironment 'AzureStackAdmin'
        $azureAccount = Initialize-AzureRmUserAccount $azureEnvironment

        foreach ($directoryTenantName in $GuestDirectoryTenantName) {
            # Resolve the guest directory tenant ID from the name
            $directoryTenantId = (New-Object uri(Invoke-RestMethod "$($azureEnvironment.ActiveDirectoryAuthority.TrimEnd('/'))/$directoryTenantName/.well-known/openid-configuration").token_endpoint).AbsolutePath.Split('/')[1]

            # Add (or update) the new directory tenant to the Azure Stack deployment
            $params = @{
                ApiVersion        = '2015-11-01'
                ResourceType      = "Microsoft.Subscriptions.Admin/directoryTenants"
                ResourceGroupName = $ResourceGroupName
                ResourceName      = $directoryTenantName
                Location          = $Location
                Properties        = @{ tenantId = $directoryTenantId }
            }
            
            # Check if resource group exists, create it if it doesn't
            $rg = Get-AzureRmResourceGroup -Name $ResourceGroupName -Location $Location -ErrorAction SilentlyContinue
            if ($rg -eq $null) {
                New-AzureRmResourceGroup -Name $ResourceGroupName -Location $Location -ErrorAction SilentlyContinue | Out-Null
            }
            
            $directoryTenant = New-AzureRmResource @params -Force -Verbose -ErrorAction Stop
            Write-Verbose -Message "Directory Tenant onboarded: $(ConvertTo-Json $directoryTenant)" -Verbose
        }
    }

    function Initialize-AzureRmEnvironment([string]$environmentName) {
        $endpoints = Invoke-RestMethod -Method Get -Uri "$($AdminResourceManagerEndpoint.ToString().TrimEnd('/'))/metadata/endpoints?api-version=2015-01-01" -Verbose
        Write-Verbose -Message "Endpoints: $(ConvertTo-Json $endpoints)" -Verbose

        # resolve the directory tenant ID from the name
        $directoryTenantId = (New-Object uri(Invoke-RestMethod "$($endpoints.authentication.loginEndpoint.TrimEnd('/'))/$DirectoryTenantName/.well-known/openid-configuration").token_endpoint).AbsolutePath.Split('/')[1]

        $azureEnvironmentParams = @{
            Name                                     = $environmentName
            ActiveDirectoryEndpoint                  = $endpoints.authentication.loginEndpoint.TrimEnd('/') + "/"
            ActiveDirectoryServiceEndpointResourceId = $endpoints.authentication.audiences[0]
            AdTenant                                 = $directoryTenantId
            ResourceManagerEndpoint                  = $AdminResourceManagerEndpoint
            GalleryEndpoint                          = $endpoints.galleryEndpoint
            GraphEndpoint                            = $endpoints.graphEndpoint
            GraphAudience                            = $endpoints.graphEndpoint
        }

        $azureEnvironment = Add-AzureRmEnvironment @azureEnvironmentParams -ErrorAction Ignore
        $azureEnvironment = Get-AzureRmEnvironment -Name $environmentName -ErrorAction Stop

        return $azureEnvironment
    }

    function Initialize-AzureRmUserAccount([Microsoft.Azure.Commands.Profile.Models.PSAzureEnvironment]$azureEnvironment) {
        $params = @{
            EnvironmentName = $azureEnvironment.Name
            TenantId        = $azureEnvironment.AdTenant
        }

        if ($AutomationCredential) {
            $params += @{ Credential = $AutomationCredential }
        }

        # Prompts the user for interactive login flow if automation credential is not specified
        #$DebugPreference = "Continue"
        $azureAccount = Add-AzureRmAccount @params

        if ($SubscriptionName) {
            Select-AzureRmSubscription -SubscriptionName $SubscriptionName | Out-Null
        }
        elseif ($SubscriptionId) {
            Select-AzureRmSubscription -SubscriptionId $SubscriptionId | Out-Null
        }

        return $azureAccount
    }

    Invoke-Main
}

<#
.Synopsis
Gets the health report of identity application in the Azure Stack home and guest directories
.DESCRIPTION
Gets the health report for Azure Stack identity applications in the home directory as well as guest directories of Azure Stack. Any directories with an unhealthy status need to have their permissions updated.
.EXAMPLE
$adminResourceManagerEndpoint = "https://adminmanagement.local.azurestack.external"
$homeDirectoryTenantName = "<homeDirectoryTenant>.onmicrosoft.com"
 
Get-AzsHealthReport -AdminResourceManagerEndpoint $adminResourceManagerEndpoint `
    -DirectoryTenantName $homeDirectoryTenantName -Verbose
#>


function Get-AzsHealthReport {
    [CmdletBinding()]
    param
    (
        # The endpoint of the Azure Stack Resource Manager service.
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [ValidateScript( { $_.Scheme -eq [System.Uri]::UriSchemeHttps })]
        [uri] $AdminResourceManagerEndpoint,

        # The name of the home Directory Tenant in which the Azure Stack Administrator subscription resides.
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $DirectoryTenantName,

        # Optional: A credential used to authenticate with Azure Stack. Must support a non-interactive authentication flow. If not provided, the script will prompt for user credentials.
        [Parameter()]
        [ValidateNotNull()]
        [pscredential] $AutomationCredential = $null
    )

    $ErrorActionPreference = 'Stop'
    $VerbosePreference = 'Continue'

    # Install-Module AzureRm
    Import-Module 'AzureRm.Profile' -Verbose:$false 4> $null
    Import-Module "$PSScriptRoot\GraphAPI\GraphAPI.psm1" -Verbose:$false 4> $null

    function Invoke-Main {
        # Initialize the Azure PowerShell module to communicate with the Azure Resource Manager in the public cloud corresponding to the Azure Stack Graph Service. Will prompt user for credentials.
        Write-Host "Authenticating user..."
        $azureStackEnvironment = Initialize-AzureRmEnvironment 'AzureStackAdmin'
        $refreshToken = Initialize-AzureRmUserAccount $azureStackEnvironment

        # Initialize the Graph PowerShell module to communicate with the correct graph service
        $graphEnvironment = Resolve-GraphEnvironment $azureStackEnvironment
        Initialize-GraphEnvironment -Environment $graphEnvironment -DirectoryTenantId $DirectoryTenantName -RefreshToken $refreshToken

        # Call Azure Stack Resource Manager to retrieve the list of registered applications which need to be initialized in the onboarding directory tenant
        Write-Host "Acquiring an access token to communicate with Resource Manager..."
        $armAccessToken = Get-ArmAccessToken $azureStackEnvironment
        
        $defaultProviderSubscription = Get-AzureRmSubscription -SubscriptionName "Default Provider Subscription"
        $healthReportUrl = "$($AdminResourceManagerEndpoint.AbsoluteUri)/subscriptions/$($defaultProviderSubscription.SubscriptionId)/providers/Microsoft.Subscriptions.Admin/checkIdentityHealth?api-version=2018-05-01"
        $headers = @{ "Authorization" = "Bearer $armAccessToken" }

        $healthReport = (Invoke-WebRequest -Headers $headers -Uri $healthReportUrl -Method Post -UseBasicParsing -TimeoutSec 40).Content | ConvertFrom-Json

        return $healthReport
    }

    function Initialize-AzureRmEnvironment([string]$environmentName) {
        $endpoints = Invoke-RestMethod -Method Get -Uri "$($AdminResourceManagerEndpoint.ToString().TrimEnd('/'))/metadata/endpoints?api-version=2015-01-01" -Verbose
        Write-Verbose -Message "Endpoints: $(ConvertTo-Json $endpoints)" -Verbose

        # resolve the directory tenant ID from the name
        $directoryTenantId = (New-Object uri(Invoke-RestMethod "$($endpoints.authentication.loginEndpoint.TrimEnd('/'))/$DirectoryTenantName/.well-known/openid-configuration").token_endpoint).AbsolutePath.Split('/')[1]

        $azureEnvironmentParams = @{
            Name                                     = $environmentName
            ActiveDirectoryEndpoint                  = $endpoints.authentication.loginEndpoint.TrimEnd('/') + "/"
            ActiveDirectoryServiceEndpointResourceId = $endpoints.authentication.audiences[0]
            AdTenant                                 = $directoryTenantId
            ResourceManagerEndpoint                  = $AdminResourceManagerEndpoint
            GalleryEndpoint                          = $endpoints.galleryEndpoint
            GraphEndpoint                            = $endpoints.graphEndpoint
            GraphAudience                            = $endpoints.graphEndpoint
        }

        $azureEnvironment = Add-AzureRmEnvironment @azureEnvironmentParams -ErrorAction Ignore
        $azureEnvironment = Get-AzureRmEnvironment -Name $environmentName -ErrorAction Stop

        return $azureEnvironment
    }

    function Initialize-AzureRmUserAccount([Microsoft.Azure.Commands.Profile.Models.PSAzureEnvironment]$azureStackEnvironment) {
        $params = @{
            EnvironmentName = $azureStackEnvironment.Name
            TenantId        = $azureStackEnvironment.AdTenant
        }

        if ($AutomationCredential) {
            $params += @{ Credential = $AutomationCredential }
        }

        # Prompts the user for interactive login flow if automation credential is not specified
        $azureStackAccount = Add-AzureRmAccount @params

        # Retrieve the refresh token
        $tokens = @()
        $tokens += try { [Microsoft.IdentityModel.Clients.ActiveDirectory.TokenCache]::DefaultShared.ReadItems() } catch { }
        $tokens += try { [Microsoft.Azure.Commands.Common.Authentication.AzureSession]::Instance.TokenCache.ReadItems() } catch { }
        $refreshToken = $tokens |
        Where Resource -EQ $azureStackEnvironment.ActiveDirectoryServiceEndpointResourceId |
        Where IsMultipleResourceRefreshToken -EQ $true |
        Where DisplayableId -EQ $azureStackAccount.Context.Account.Id |
        Sort ExpiresOn |
        Select -Last 1 -ExpandProperty RefreshToken |
        ConvertTo-SecureString -AsPlainText -Force

        # Workaround due to regression in AzurePowerShell profile module which fails to populate the response object of "Add-AzureRmAccount" cmdlet
        if (-not $refreshToken) {
            if ($tokens.Count -eq 1) {
                Write-Warning "Failed to find target refresh token from Azure PowerShell Cache; attempting to reuse the single cached auth context..."
                $refreshToken = $tokens[0].RefreshToken | ConvertTo-SecureString -AsPlainText -Force
            }
            else {
                throw "Unable to find refresh token from Azure PowerShell Cache. Please try the command again in a fresh PowerShell instance after running 'Clear-AzureRmContext -Scope CurrentUser -Force -Verbose'."
            }
        }

        return $refreshToken
    }

    function Resolve-GraphEnvironment([Microsoft.Azure.Commands.Profile.Models.PSAzureEnvironment]$azureEnvironment) {
        $graphEnvironment = switch ($azureEnvironment.ActiveDirectoryAuthority) {
            'https://login.microsoftonline.com/' { 'AzureCloud' }
            'https://login.chinacloudapi.cn/' { 'AzureChinaCloud' }
            'https://login-us.microsoftonline.com/' { 'AzureUSGovernment' }
            'https://login.microsoftonline.us/' { 'AzureUSGovernment' }
            'https://login.microsoftonline.de/' { 'AzureGermanCloud' }

            Default { throw "Unsupported graph resource identifier: $_" }
        }

        return $graphEnvironment
    }

    function Get-ArmAccessToken([Microsoft.Azure.Commands.Profile.Models.PSAzureEnvironment]$azureStackEnvironment) {
        $armAccessToken = $null
        $attempts = 0
        $maxAttempts = 12
        $delayInSeconds = 5
        do {
            try {
                $attempts++
                $armAccessToken = (Get-GraphToken -Resource $azureStackEnvironment.ActiveDirectoryServiceEndpointResourceId -UseEnvironmentData -ErrorAction Stop).access_token
            }
            catch {
                if ($attempts -ge $maxAttempts) {
                    throw
                }
                Write-Verbose "Error attempting to acquire ARM access token: $_`r`n$($_.Exception)" -Verbose
                Write-Verbose "Delaying for $delayInSeconds seconds before trying again... (attempt $attempts/$maxAttempts)" -Verbose
                Start-Sleep -Seconds $delayInSeconds
            }
        }
        while (-not $armAccessToken)

        return $armAccessToken
    }

    $logFile = Join-Path -Path $PSScriptRoot -ChildPath "$DirectoryTenantName.$(Get-Date -Format MM-dd_HH-mm-ss_ms).log"
    Write-Verbose "Logging additional information to log file '$logFile'" -Verbose

    $logStartMessage = "[$(Get-Date -Format 'hh:mm:ss tt')] - Beginning invocation of '$($MyInvocation.InvocationName)' with parameters: $(ConvertTo-Json $PSBoundParameters -Depth 4)"
    $logStartMessage >> $logFile

    try {
        # Redirect verbose output to a log file
        Invoke-Main 4>> $logFile

        $logEndMessage = "[$(Get-Date -Format 'hh:mm:ss tt')] - Script completed successfully."
        $logEndMessage >> $logFile
    }
    catch {
        $logErrorMessage = "[$(Get-Date -Format 'hh:mm:ss tt')] - Script terminated with error: $_`r`n$($_.Exception)"
        $logErrorMessage >> $logFile
        Write-Warning "An error has occurred; more information may be found in the log file '$logFile'" -WarningAction Continue
        throw
    }
}

<#
.Synopsis
Consents to any missing permissions for Azure Stack identity applications in the home directory of Azure Stack.
.DESCRIPTION
Consents to any missing permissions for Azure Stack identity applications in the home directory of Azure Stack. This is needed to complete the "installation" of new Resource Provider identity applications in Azure Stack.
.EXAMPLE
$adminResourceManagerEndpoint = "https://adminmanagement.local.azurestack.external"
$homeDirectoryTenantName = "<homeDirectoryTenant>.onmicrosoft.com"
 
Update-AzsHomeDirectoryTenant -AdminResourceManagerEndpoint $adminResourceManagerEndpoint `
    -DirectoryTenantName $homeDirectoryTenantName -Verbose
#>


function Update-AzsHomeDirectoryTenant {
    [CmdletBinding()]
    param
    (
        # The endpoint of the Azure Stack Resource Manager service.
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [ValidateScript( { $_.Scheme -eq [System.Uri]::UriSchemeHttps })]
        [uri] $AdminResourceManagerEndpoint,

        # The name of the home Directory Tenant in which the Azure Stack Administrator subscription resides.
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $DirectoryTenantName,

        # Optional: A credential used to authenticate with Azure Stack. Must support a non-interactive authentication flow. If not provided, the script will prompt for user credentials.
        [Parameter()]
        [ValidateNotNull()]
        [pscredential] $AutomationCredential = $null
    )

    $ErrorActionPreference = 'Stop'
    $VerbosePreference = 'Continue'

    Import-Module 'AzureRm.Profile' -Verbose:$false 4> $null
    Import-Module "$PSScriptRoot\GraphAPI\GraphAPI.psm1" -Verbose:$false 4> $null

    function Invoke-Main {
        # Initialize the Azure PowerShell module to communicate with the Azure Resource Manager in the public cloud corresponding to the Azure Stack Graph Service. Will prompt user for credentials.
        Write-Host "Authenticating user..."
        $azureStackEnvironment = Initialize-AzureRmEnvironment 'AzureStackAdmin'
        $refreshToken = Initialize-AzureRmUserAccount $azureStackEnvironment

        # Initialize the Graph PowerShell module to communicate with the correct graph service
        $graphEnvironment = Resolve-GraphEnvironment $azureStackEnvironment
        Initialize-GraphEnvironment -Environment $graphEnvironment -DirectoryTenantId $DirectoryTenantName -RefreshToken $refreshToken

        # Call Azure Stack Resource Manager to retrieve the list of registered applications which need to be initialized in the onboarding directory tenant
        Write-Host "Acquiring an access token to communicate with Resource Manager..."
        $armAccessToken = Get-ArmAccessToken $azureStackEnvironment

        Write-Host "Looking-up the registered identity applications which need to be installed in your directory..."
        $applicationRegistrationParams = @{
            Method  = [Microsoft.PowerShell.Commands.WebRequestMethod]::Get
            Headers = @{ Authorization = "Bearer $armAccessToken" }
            Uri     = "$($AdminResourceManagerEndpoint.ToString().TrimEnd('/'))/applicationRegistrations?api-version=2014-04-01-preview"
        }
        $applicationRegistrations = Invoke-RestMethod @applicationRegistrationParams | Select -ExpandProperty value

        # Identify which permissions have already been granted to each registered application and which additional permissions need to be granted
        $permissions = @()
        $count = 0
        foreach ($applicationRegistration in $applicationRegistrations) {
            # Initialize the service principal for the registered application
            $count++
            $applicationServicePrincipal = Initialize-GraphApplicationServicePrincipal -ApplicationId $applicationRegistration.appId
            Write-Host "Installing Application... ($($count) of $($applicationRegistrations.Count)): $($applicationServicePrincipal.appId) '$($applicationServicePrincipal.appDisplayName)'"

            # WORKAROUND - the recent Azure Stack update has a missing permission registration; temporarily "inject" this permission registration into the returned data
            if ($applicationServicePrincipal.servicePrincipalNames | Where { $_ -like 'https://deploymentprovider.*/*' }) {
                Write-Verbose "Adding missing permission registrations for application '$($applicationServicePrincipal.appDisplayName)' ($($applicationServicePrincipal.appId))..." -Verbose

                $graph = Get-GraphApplicationServicePrincipal -ApplicationId (Get-GraphEnvironmentInfo).Applications.WindowsAzureActiveDirectory.Id

                $applicationRegistration.appRoleAssignments = @(
                    [pscustomobject]@{
                        resource = (Get-GraphEnvironmentInfo).Applications.WindowsAzureActiveDirectory.Id
                        client   = $applicationRegistration.appId
                        roleId   = $graph.appRoles | Where value -EQ 'Directory.Read.All' | Select -ExpandProperty id
                    },

                    [pscustomobject]@{
                        resource = (Get-GraphEnvironmentInfo).Applications.WindowsAzureActiveDirectory.Id
                        client   = $applicationRegistration.appId
                        roleId   = $graph.appRoles | Where value -EQ 'Application.ReadWrite.OwnedBy' | Select -ExpandProperty id
                    }
                )
            }

            # Initialize the necessary tags for the registered application
            if ($applicationRegistration.tags) {
                Update-GraphApplicationServicePrincipalTags -ApplicationId $applicationRegistration.appId -Tags $applicationRegistration.tags
            }

            # Lookup the permission consent status for the *application* permissions (either to or from) which the registered application requires
            foreach ($appRoleAssignment in $applicationRegistration.appRoleAssignments) {
                $params = @{
                    ClientApplicationId   = $appRoleAssignment.client
                    ResourceApplicationId = $appRoleAssignment.resource
                    PermissionType        = 'Application'
                    PermissionId          = $appRoleAssignment.roleId
                }
                $permissions += New-GraphPermissionDescription @params -LookupConsentStatus
            }

            # Lookup the permission consent status for the *delegated* permissions (either to or from) which the registered application requires
            foreach ($oauth2PermissionGrant in $applicationRegistration.oauth2PermissionGrants) {
                $resourceApplicationServicePrincipal = Initialize-GraphApplicationServicePrincipal -ApplicationId $oauth2PermissionGrant.resource
                foreach ($scope in $oauth2PermissionGrant.scope.Split(' ')) {
                    $params = @{
                        ClientApplicationId                 = $oauth2PermissionGrant.client
                        ResourceApplicationServicePrincipal = $resourceApplicationServicePrincipal
                        PermissionType                      = 'Delegated'
                        PermissionId                        = ($resourceApplicationServicePrincipal.oauth2Permissions | Where value -EQ $scope).id
                    }
                    $permissions += New-GraphPermissionDescription @params -LookupConsentStatus
                }
            }
        }

        # Trace the permission status
        Write-Verbose "Current permission status: $($permissions | ConvertTo-Json -Depth 4)" -Verbose

        $permissionFile = Join-Path -Path $PSScriptRoot -ChildPath "$DirectoryTenantName.permissions.json"
        $permissionContent = $permissions | Select -Property * -ExcludeProperty isConsented | ConvertTo-Json -Depth 4 | Out-String
        $permissionContent > $permissionFile

        # Display application status to user
        $permissionsByClient = $permissions | Select *, @{n = 'Client'; e = { '{0} {1}' -f $_.clientApplicationId, $_.clientApplicationDisplayName } } | Sort clientApplicationDisplayName | Group Client
        $readyApplications = @()
        $pendingApplications = @()
        foreach ($client in $permissionsByClient) {
            if ($client.Group.isConsented -Contains $false) {
                $pendingApplications += $client
            }
            else {
                $readyApplications += $client
            }
        }

        Write-Host ""
        if ($readyApplications) {
            Write-Host "Applications installed and configured:"
            Write-Host "`t$($readyApplications.Name -join "`r`n`t")"
        }
        if ($readyApplications -and $pendingApplications) {
            Write-Host ""
        }
        if ($pendingApplications) {
            Write-Host "Applications waiting to be configured:"
            Write-Host "`t$($pendingApplications.Name -join "`r`n`t")"
        }
        Write-Host ""

        # Grant any missing permissions for registered applications
        if ($permissions | Where isConsented -EQ $false | Select -First 1) {
            Write-Host "Configuring applications... (this may take up to a few minutes to complete)"
            Write-Host ""
            $permissions | Where isConsented -EQ $false | Grant-GraphApplicationPermission
        }

        Write-Host "All applications installed and configured! Your home directory '$DirectoryTenantName' has been successfully updated to be used with Azure Stack!"
        Write-Host ""
        Write-Host "A more detailed description of the applications installed and with what permissions they have been configured can be found in the file '$permissionFile'."
        Write-Host "Run this script again at any time to check the status of the Azure Stack applications in your directory."
        Write-Warning "If your Azure Stack Administrator installs new services or updates in the future, you may need to run this script again."
    }

    function Initialize-AzureRmEnvironment([string]$environmentName) {
        $endpoints = Invoke-RestMethod -Method Get -Uri "$($AdminResourceManagerEndpoint.ToString().TrimEnd('/'))/metadata/endpoints?api-version=2015-01-01" -Verbose
        Write-Verbose -Message "Endpoints: $(ConvertTo-Json $endpoints)" -Verbose

        # resolve the directory tenant ID from the name
        $directoryTenantId = (New-Object uri(Invoke-RestMethod "$($endpoints.authentication.loginEndpoint.TrimEnd('/'))/$DirectoryTenantName/.well-known/openid-configuration").token_endpoint).AbsolutePath.Split('/')[1]

        $azureEnvironmentParams = @{
            Name                                     = $environmentName
            ActiveDirectoryEndpoint                  = $endpoints.authentication.loginEndpoint.TrimEnd('/') + "/"
            ActiveDirectoryServiceEndpointResourceId = $endpoints.authentication.audiences[0]
            AdTenant                                 = $directoryTenantId
            ResourceManagerEndpoint                  = $AdminResourceManagerEndpoint
            GalleryEndpoint                          = $endpoints.galleryEndpoint
            GraphEndpoint                            = $endpoints.graphEndpoint
            GraphAudience                            = $endpoints.graphEndpoint
        }

        $azureEnvironment = Add-AzureRmEnvironment @azureEnvironmentParams -ErrorAction Ignore
        $azureEnvironment = Get-AzureRmEnvironment -Name $environmentName -ErrorAction Stop

        return $azureEnvironment
    }

    function Initialize-AzureRmUserAccount([Microsoft.Azure.Commands.Profile.Models.PSAzureEnvironment]$azureStackEnvironment) {
        $params = @{
            EnvironmentName = $azureStackEnvironment.Name
            TenantId        = $azureStackEnvironment.AdTenant
        }

        if ($AutomationCredential) {
            $params += @{ Credential = $AutomationCredential }
        }

        # Prompts the user for interactive login flow if automation credential is not specified
        $azureStackAccount = Add-AzureRmAccount @params

        # Retrieve the refresh token
        $tokens = @()
        $tokens += try { [Microsoft.IdentityModel.Clients.ActiveDirectory.TokenCache]::DefaultShared.ReadItems() } catch { }
        $tokens += try { [Microsoft.Azure.Commands.Common.Authentication.AzureSession]::Instance.TokenCache.ReadItems() } catch { }
        $refreshToken = $tokens |
        Where Resource -EQ $azureStackEnvironment.ActiveDirectoryServiceEndpointResourceId |
        Where IsMultipleResourceRefreshToken -EQ $true |
        Where DisplayableId -EQ $azureStackAccount.Context.Account.Id |
        Sort ExpiresOn |
        Select -Last 1 -ExpandProperty RefreshToken |
        ConvertTo-SecureString -AsPlainText -Force

        # Workaround due to regression in AzurePowerShell profile module which fails to populate the response object of "Add-AzureRmAccount" cmdlet
        if (-not $refreshToken) {
            if ($tokens.Count -eq 1) {
                Write-Warning "Failed to find target refresh token from Azure PowerShell Cache; attempting to reuse the single cached auth context..."
                $refreshToken = $tokens[0].RefreshToken | ConvertTo-SecureString -AsPlainText -Force
            }
            else {
                throw "Unable to find refresh token from Azure PowerShell Cache. Please try the command again in a fresh PowerShell instance after running 'Clear-AzureRmContext -Scope CurrentUser -Force -Verbose'."
            }
        }

        return $refreshToken
    }

    function Resolve-GraphEnvironment([Microsoft.Azure.Commands.Profile.Models.PSAzureEnvironment]$azureEnvironment) {
        $graphEnvironment = switch ($azureEnvironment.ActiveDirectoryAuthority) {
            'https://login.microsoftonline.com/' { 'AzureCloud' }
            'https://login.chinacloudapi.cn/' { 'AzureChinaCloud' }
            'https://login-us.microsoftonline.com/' { 'AzureUSGovernment' }
            'https://login.microsoftonline.us/' { 'AzureUSGovernment' }
            'https://login.microsoftonline.de/' { 'AzureGermanCloud' }

            Default { throw "Unsupported graph resource identifier: $_" }
        }

        return $graphEnvironment
    }

    function Get-ArmAccessToken([Microsoft.Azure.Commands.Profile.Models.PSAzureEnvironment]$azureStackEnvironment) {
        $armAccessToken = $null
        $attempts = 0
        $maxAttempts = 12
        $delayInSeconds = 5
        do {
            try {
                $attempts++
                $armAccessToken = (Get-GraphToken -Resource $azureStackEnvironment.ActiveDirectoryServiceEndpointResourceId -UseEnvironmentData -ErrorAction Stop).access_token
            }
            catch {
                if ($attempts -ge $maxAttempts) {
                    throw
                }
                Write-Verbose "Error attempting to acquire ARM access token: $_`r`n$($_.Exception)" -Verbose
                Write-Verbose "Delaying for $delayInSeconds seconds before trying again... (attempt $attempts/$maxAttempts)" -Verbose
                Start-Sleep -Seconds $delayInSeconds
            }
        }
        while (-not $armAccessToken)

        return $armAccessToken
    }

    $logFile = Join-Path -Path $PSScriptRoot -ChildPath "$DirectoryTenantName.$(Get-Date -Format MM-dd_HH-mm-ss_ms).log"
    Write-Verbose "Logging additional information to log file '$logFile'" -Verbose

    $logStartMessage = "[$(Get-Date -Format 'hh:mm:ss tt')] - Beginning invocation of '$($MyInvocation.InvocationName)' with parameters: $(ConvertTo-Json $PSBoundParameters -Depth 4)"
    $logStartMessage >> $logFile

    try {
        # Redirect verbose output to a log file
        Invoke-Main 4>> $logFile

        $logEndMessage = "[$(Get-Date -Format 'hh:mm:ss tt')] - Script completed successfully."
        $logEndMessage >> $logFile
    }
    catch {
        $logErrorMessage = "[$(Get-Date -Format 'hh:mm:ss tt')] - Script terminated with error: $_`r`n$($_.Exception)"
        $logErrorMessage >> $logFile
        Write-Warning "An error has occurred; more information may be found in the log file '$logFile'" -WarningAction Continue
        throw
    }
}

<#
.Synopsis
Consents to the given Azure Stack instance within the callers's Azure Directory Tenant.
.DESCRIPTION
Consents to the given Azure Stack instance within the callers's Azure Directory Tenant. This is needed to propagate Azure Stack applications into the user's directory tenant.
.EXAMPLE
$tenantARMEndpoint = "https://management.local.azurestack.external"
$myDirectoryTenantName = "<guestDirectoryTenant>.onmicrosoft.com"
 
Register-AzsWithMyDirectoryTenant -TenantResourceManagerEndpoint $tenantARMEndpoint `
    -DirectoryTenantName $myDirectoryTenantName -Verbose -Debug
#>


function Register-AzsWithMyDirectoryTenant {
    [CmdletBinding()]
    param
    (
        # The endpoint of the Azure Stack Resource Manager service.
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [ValidateScript( { $_.Scheme -eq [System.Uri]::UriSchemeHttps })]
        [uri] $TenantResourceManagerEndpoint,

        # The name of the directory tenant being onboarded.
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $DirectoryTenantName,

        # Optional: A credential used to authenticate with Azure Stack. Must support a non-interactive authentication flow. If not provided, the script will prompt for user credentials.
        [Parameter()]
        [ValidateNotNull()]
        [pscredential] $AutomationCredential = $null
    )

    $ErrorActionPreference = 'Stop'
    $VerbosePreference = 'Continue'

    Import-Module 'AzureRm.Profile' -Verbose:$false 4> $null
    Import-Module "$PSScriptRoot\GraphAPI\GraphAPI.psm1" -Verbose:$false 4> $null

    function Invoke-Main {
        # Initialize the Azure PowerShell module to communicate with the Azure Resource Manager in the public cloud corresponding to the Azure Stack Graph Service. Will prompt user for credentials.
        Write-Host "Authenticating user..."
        $azureStackEnvironment = Initialize-AzureRmEnvironment 'AzureStack'
        $azureEnvironment = Resolve-AzureEnvironment $azureStackEnvironment
        $refreshToken = Initialize-AzureRmUserAccount $azureEnvironment $azureStackEnvironment.AdTenant

        # Initialize the Graph PowerShell module to communicate with the correct graph service
        $graphEnvironment = Resolve-GraphEnvironment $azureEnvironment
        Initialize-GraphEnvironment -Environment $graphEnvironment -DirectoryTenantId $DirectoryTenantName -RefreshToken $refreshToken

        # Initialize the service principal for the Azure Stack Resource Manager application
        Write-Host "Installing Resource Manager in your directory ('$DirectoryTenantName')..."
        $resourceManagerServicePrincipal = Initialize-ResourceManagerServicePrincipal

        # Authorize the Azure Powershell module to act as a client to call the Azure Stack Resource Manager in the onboarding directory tenant
        Write-Host "Authorizing the Azure PowerShell module to communicate with Resource Manager in your directory..."
        Initialize-GraphOAuth2PermissionGrant -ClientApplicationId (Get-GraphEnvironmentInfo).Applications.PowerShell.Id -ResourceApplicationIdentifierUri $azureStackEnvironment.ActiveDirectoryServiceEndpointResourceId

        # Call Azure Stack Resource Manager to retrieve the list of registered applications which need to be initialized in the onboarding directory tenant
        Write-Host "Acquiring an access token to communicate with Resource Manager... (this may take up to a few minutes to complete)"
        $armAccessToken = Get-ArmAccessToken $azureStackEnvironment

        Write-Host "Looking-up the registered identity applications which need to be installed in your directory..."
        $applicationRegistrationParams = @{
            Method  = [Microsoft.PowerShell.Commands.WebRequestMethod]::Get
            Headers = @{ Authorization = "Bearer $armAccessToken" }
            Uri     = "$($TenantResourceManagerEndpoint.ToString().TrimEnd('/'))/applicationRegistrations?api-version=2014-04-01-preview"
        }
        $applicationRegistrations = Invoke-RestMethod @applicationRegistrationParams | Select -ExpandProperty value

        # Identify which permissions have already been granted to each registered application and which additional permissions need to be granted
        $permissions = @()
        $count = 0
        foreach ($applicationRegistration in $applicationRegistrations) {
            # Initialize the service principal for the registered application
            $count++
            $applicationServicePrincipal = Initialize-GraphApplicationServicePrincipal -ApplicationId $applicationRegistration.appId
            Write-Host "Installing Application... ($($count) of $($applicationRegistrations.Count)): $($applicationServicePrincipal.appId) '$($applicationServicePrincipal.appDisplayName)'"

            # Initialize the necessary tags for the registered application
            if ($applicationRegistration.tags) {
                Update-GraphApplicationServicePrincipalTags -ApplicationId $applicationRegistration.appId -Tags $applicationRegistration.tags
            }

            # Lookup the permission consent status for the *application* permissions (either to or from) which the registered application requires
            foreach ($appRoleAssignment in $applicationRegistration.appRoleAssignments) {
                $params = @{
                    ClientApplicationId   = $appRoleAssignment.client
                    ResourceApplicationId = $appRoleAssignment.resource
                    PermissionType        = 'Application'
                    PermissionId          = $appRoleAssignment.roleId
                }
                $permissions += New-GraphPermissionDescription @params -LookupConsentStatus
            }

            # Lookup the permission consent status for the *delegated* permissions (either to or from) which the registered application requires
            foreach ($oauth2PermissionGrant in $applicationRegistration.oauth2PermissionGrants) {
                $resourceApplicationServicePrincipal = Initialize-GraphApplicationServicePrincipal -ApplicationId $oauth2PermissionGrant.resource
                foreach ($scope in $oauth2PermissionGrant.scope.Split(' ')) {
                    $params = @{
                        ClientApplicationId                 = $oauth2PermissionGrant.client
                        ResourceApplicationServicePrincipal = $resourceApplicationServicePrincipal
                        PermissionType                      = 'Delegated'
                        PermissionId                        = ($resourceApplicationServicePrincipal.oauth2Permissions | Where value -EQ $scope).id
                    }
                    $permissions += New-GraphPermissionDescription @params -LookupConsentStatus
                }
            }
        }

        # Trace the permission status
        Write-Verbose "Current permission status: $($permissions | ConvertTo-Json -Depth 4)" -Verbose

        $permissionFile = Join-Path -Path $PSScriptRoot -ChildPath "$DirectoryTenantName.permissions.json"
        $permissionContent = $permissions | Select -Property * -ExcludeProperty isConsented | ConvertTo-Json -Depth 4 | Out-String
        $permissionContent > $permissionFile

        # Display application status to user
        $permissionsByClient = $permissions | Select *, @{n = 'Client'; e = { '{0} {1}' -f $_.clientApplicationId, $_.clientApplicationDisplayName } } | Sort clientApplicationDisplayName | Group Client
        $readyApplications = @()
        $pendingApplications = @()
        foreach ($client in $permissionsByClient) {
            if ($client.Group.isConsented -Contains $false) {
                $pendingApplications += $client
            }
            else {
                $readyApplications += $client
            }
        }

        Write-Host ""
        if ($readyApplications) {
            Write-Host "Applications installed and configured:"
            Write-Host "`t$($readyApplications.Name -join "`r`n`t")"
        }
        if ($readyApplications -and $pendingApplications) {
            Write-Host ""
        }
        if ($pendingApplications) {
            Write-Host "Applications waiting to be configured:"
            Write-Host "`t$($pendingApplications.Name -join "`r`n`t")"
        }
        Write-Host ""

        # Grant any missing permissions for registered applications
        if ($permissions | Where isConsented -EQ $false | Select -First 1) {
            Write-Host "Configuring applications... (this may take up to a few minutes to complete)"
            Write-Host ""
            $permissions | Where isConsented -EQ $false | Grant-GraphApplicationPermission
        }

        Write-Host "All applications installed and configured! Your directory '$DirectoryTenantName' has been successfully onboarded and can now be used with Azure Stack!"
        Write-Host ""
        Write-Host "A more detailed description of the applications installed and with what permissions they have been configured can be found in the file '$permissionFile'."
        Write-Host "Run this script again at any time to check the status of the Azure Stack applications in your directory."
        Write-Warning "If your Azure Stack Administrator installs new services or updates in the future, you may need to run this script again."
    }

    function Initialize-AzureRmEnvironment([string]$environmentName) {
        $endpoints = Invoke-RestMethod -Method Get -Uri "$($TenantResourceManagerEndpoint.ToString().TrimEnd('/'))/metadata/endpoints?api-version=2015-01-01" -Verbose
        Write-Verbose -Message "Endpoints: $(ConvertTo-Json $endpoints)" -Verbose

        # resolve the directory tenant ID from the name
        $directoryTenantId = (New-Object uri(Invoke-RestMethod "$($endpoints.authentication.loginEndpoint.TrimEnd('/'))/$DirectoryTenantName/.well-known/openid-configuration").token_endpoint).AbsolutePath.Split('/')[1]

        $azureEnvironmentParams = @{
            Name                                     = $environmentName
            ActiveDirectoryEndpoint                  = $endpoints.authentication.loginEndpoint.TrimEnd('/') + "/"
            ActiveDirectoryServiceEndpointResourceId = $endpoints.authentication.audiences[0]
            AdTenant                                 = $directoryTenantId
            ResourceManagerEndpoint                  = $TenantResourceManagerEndpoint
            GalleryEndpoint                          = $endpoints.galleryEndpoint
            GraphEndpoint                            = $endpoints.graphEndpoint
            GraphAudience                            = $endpoints.graphEndpoint
        }

        $azureEnvironment = Add-AzureRmEnvironment @azureEnvironmentParams -ErrorAction Ignore
        $azureEnvironment = Get-AzureRmEnvironment -Name $environmentName -ErrorAction Stop

        return $azureEnvironment
    }

    function Resolve-AzureEnvironment([Microsoft.Azure.Commands.Profile.Models.PSAzureEnvironment]$azureStackEnvironment) {
        $azureEnvironment = Get-AzureRmEnvironment |
        Where GraphEndpointResourceId -EQ $azureStackEnvironment.GraphEndpointResourceId |
        Where Name -In @('AzureCloud', 'AzureChinaCloud', 'AzureUSGovernment', 'AzureGermanCloud')

        # Differentiate between AzureCloud and AzureUSGovernment
        if ($azureEnvironment.Count -ge 2) {
            $name = if ($azureStackEnvironment.ActiveDirectoryAuthority -eq 'https://login-us.microsoftonline.com/' -or $azureStackEnvironment.ActiveDirectoryAuthority -eq 'https://login.microsoftonline.us/') { 'AzureUSGovernment' } else { 'AzureCloud' }
            $azureEnvironment = $azureEnvironment | Where Name -EQ $name
        }

        return $azureEnvironment
    }

    function Initialize-AzureRmUserAccount([Microsoft.Azure.Commands.Profile.Models.PSAzureEnvironment]$azureEnvironment, [string]$directoryTenantId) {
        $params = @{
            EnvironmentName = $azureEnvironment.Name
            TenantId        = $directoryTenantId
        }

        if ($AutomationCredential) {
            $params += @{ Credential = $AutomationCredential }
        }

        # Prompts the user for interactive login flow if automation credential is not specified
        $azureAccount = Add-AzureRmAccount @params

        # Retrieve the refresh token
        $tokens = @()
        $tokens += try { [Microsoft.IdentityModel.Clients.ActiveDirectory.TokenCache]::DefaultShared.ReadItems() } catch { }
        $tokens += try { [Microsoft.Azure.Commands.Common.Authentication.AzureSession]::Instance.TokenCache.ReadItems() } catch { }
        $refreshToken = $tokens |
        Where Resource -EQ $azureEnvironment.ActiveDirectoryServiceEndpointResourceId |
        Where IsMultipleResourceRefreshToken -EQ $true |
        Where DisplayableId -EQ $azureAccount.Context.Account.Id |
        Sort ExpiresOn |
        Select -Last 1 -ExpandProperty RefreshToken |
        ConvertTo-SecureString -AsPlainText -Force

        # Workaround due to regression in AzurePowerShell profile module which fails to populate the response object of "Add-AzureRmAccount" cmdlet
        if (-not $refreshToken) {
            if ($tokens.Count -eq 1) {
                Write-Warning "Failed to find target refresh token from Azure PowerShell Cache; attempting to reuse the single cached auth context..."
                $refreshToken = $tokens[0].RefreshToken | ConvertTo-SecureString -AsPlainText -Force
            }
            else {
                throw "Unable to find refresh token from Azure PowerShell Cache. Please try the command again in a fresh PowerShell instance after running 'Clear-AzureRmContext -Scope CurrentUser -Force -Verbose'."
            }
        }

        return $refreshToken
    }

    function Resolve-GraphEnvironment([Microsoft.Azure.Commands.Profile.Models.PSAzureEnvironment]$azureEnvironment) {
        $graphEnvironment = switch ($azureEnvironment.ActiveDirectoryAuthority) {
            'https://login.microsoftonline.com/' { 'AzureCloud' }
            'https://login.chinacloudapi.cn/' { 'AzureChinaCloud' }
            'https://login-us.microsoftonline.com/' { 'AzureUSGovernment' }
            'https://login.microsoftonline.us/' { 'AzureUSGovernment' }
            'https://login.microsoftonline.de/' { 'AzureGermanCloud' }

            Default { throw "Unsupported graph resource identifier: $_" }
        }

        return $graphEnvironment
    }

    function Initialize-ResourceManagerServicePrincipal {
        $identityInfo = Invoke-RestMethod -Method Get -Uri "$($TenantResourceManagerEndpoint.ToString().TrimEnd('/'))/metadata/identity?api-version=2015-01-01" -Verbose
        Write-Verbose -Message "Resource Manager identity information: $(ConvertTo-Json $identityInfo)" -Verbose

        $resourceManagerServicePrincipal = Initialize-GraphApplicationServicePrincipal -ApplicationId $identityInfo.applicationId -Verbose

        return $resourceManagerServicePrincipal
    }

    function Get-ArmAccessToken([Microsoft.Azure.Commands.Profile.Models.PSAzureEnvironment]$azureStackEnvironment) {
        $armAccessToken = $null
        $attempts = 0
        $maxAttempts = 12
        $delayInSeconds = 5
        do {
            try {
                $attempts++
                $armAccessToken = (Get-GraphToken -Resource $azureStackEnvironment.ActiveDirectoryServiceEndpointResourceId -UseEnvironmentData -ErrorAction Stop).access_token
            }
            catch {
                if ($attempts -ge $maxAttempts) {
                    throw
                }
                Write-Verbose "Error attempting to acquire ARM access token: $_`r`n$($_.Exception)" -Verbose
                Write-Verbose "Delaying for $delayInSeconds seconds before trying again... (attempt $attempts/$maxAttempts)" -Verbose
                Start-Sleep -Seconds $delayInSeconds
            }
        }
        while (-not $armAccessToken)

        return $armAccessToken
    }

    $logFile = Join-Path -Path $PSScriptRoot -ChildPath "$DirectoryTenantName.$(Get-Date -Format MM-dd_HH-mm-ss_ms).log"
    Write-Verbose "Logging additional information to log file '$logFile'" -Verbose

    $logStartMessage = "[$(Get-Date -Format 'hh:mm:ss tt')] - Beginning invocation of '$($MyInvocation.InvocationName)' with parameters: $(ConvertTo-Json $PSBoundParameters -Depth 4)"
    $logStartMessage >> $logFile

    try {
        # Redirect verbose output to a log file
        Invoke-Main 4>> $logFile

        $logEndMessage = "[$(Get-Date -Format 'hh:mm:ss tt')] - Script completed successfully."
        $logEndMessage >> $logFile
    }
    catch {
        $logErrorMessage = "[$(Get-Date -Format 'hh:mm:ss tt')] - Script terminated with error: $_`r`n$($_.Exception)"
        $logErrorMessage >> $logFile
        Write-Warning "An error has occurred; more information may be found in the log file '$logFile'" -WarningAction Continue
        throw
    }
}

<#
.Synopsis
Removes a Guest Directory Tenant from Azure Stack.
.DESCRIPTION
Running this cmdlet will remove the specified directory tenant from the Azure Stack whitelist.
Ensure an Admin of the directory tenant has already run "Unregister-AzsWithMyDirectoryTenant" or they will be unable to
complete that cleanup of their directory tenant (this cmdlet will remove the permissions they need to query Azure Stack to determine what to delete).
.EXAMPLE
$adminARMEndpoint = "https://adminmanagement.local.azurestack.external"
$azureStackDirectoryTenant = "<homeDirectoryTenant>.onmicrosoft.com"
$guestDirectoryTenantToBeOnboarded = "<guestDirectoryTenant>.onmicrosoft.com"
 
Unregister-AzsGuestDirectoryTenant -AdminResourceManagerEndpoint $adminARMEndpoint -DirectoryTenantName $azureStackDirectoryTenant -GuestDirectoryTenantName $guestDirectoryTenantToBeOnboarded
#>


function Unregister-AzsGuestDirectoryTenant {
    [CmdletBinding()]
    param
    (
        # The endpoint of the Azure Stack Resource Manager service.
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [ValidateScript( { $_.Scheme -eq [System.Uri]::UriSchemeHttps })]
        [uri] $AdminResourceManagerEndpoint,

        # The name of the home Directory Tenant in which the Azure Stack Administrator subscription resides.
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $DirectoryTenantName,

        # The name of the guest Directory Tenant which is to be decommissioned.
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $GuestDirectoryTenantName,

        # The name of the resource group in which the directory tenant resource was created.
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $ResourceGroupName = $null,

        # The identifier of the Administrator Subscription. If not specified, the script will attempt to use the set default subscription.
        [Parameter()]
        [ValidateNotNull()]
        [string] $SubscriptionId = $null,

        # The display name of the Administrator Subscription. If not specified, the script will attempt to use the set default subscription.
        [Parameter()]
        [ValidateNotNull()]
        [string] $SubscriptionName = $null,

        # Optional: A credential used to authenticate with Azure Stack. Must support a non-interactive authentication flow. If not provided, the script will prompt for user credentials.
        [Parameter()]
        [ValidateNotNull()]
        [pscredential] $AutomationCredential = $null
    )

    $ErrorActionPreference = 'Stop'
    $VerbosePreference = 'Continue'

    $ResourceManagerEndpoint = $AdminResourceManagerEndpoint

    Import-Module 'AzureRm.Profile' -Verbose:$false 4> $null

    function Invoke-Main {
        Write-DecommissionImplicationsWarning

        # Initialize the Azure PowerShell module to communicate with Azure Stack. Will prompt user for credentials.
        $azureEnvironment = Initialize-AzureRmEnvironment 'AzureStackAdmin'
        $azureAccount = Initialize-AzureRmUserAccount $azureEnvironment

        # Remove the new directory tenant to the Azure Stack deployment
        $params = @{
            ResourceId = "/subscriptions/$($azureAccount.Context.Subscription.Id)/resourceGroups/$ResourceGroupName/providers/Microsoft.Subscriptions.Admin/directoryTenants/$GuestDirectoryTenantName"
            ApiVersion = '2015-11-01'
        }
        $output = Remove-AzureRmResource @params -Force -Verbose -ErrorAction Stop
        Write-Verbose -Message "Directory Tenant decommissioned: $($params.ResourceId)" -Verbose
    }

    function Initialize-AzureRmEnvironment([string]$environmentName) {
        $endpoints = Invoke-RestMethod -Method Get -Uri "$($ResourceManagerEndpoint.ToString().TrimEnd('/'))/metadata/endpoints?api-version=2015-01-01" -Verbose
        Write-Verbose -Message "Endpoints: $(ConvertTo-Json $endpoints)" -Verbose

        # resolve the directory tenant ID from the name
        $directoryTenantId = (New-Object uri(Invoke-RestMethod "$($endpoints.authentication.loginEndpoint.TrimEnd('/'))/$DirectoryTenantName/.well-known/openid-configuration").token_endpoint).AbsolutePath.Split('/')[1]

        $azureEnvironmentParams = @{
            Name                                     = $environmentName
            ActiveDirectoryEndpoint                  = $endpoints.authentication.loginEndpoint.TrimEnd('/') + "/"
            ActiveDirectoryServiceEndpointResourceId = $endpoints.authentication.audiences[0]
            AdTenant                                 = $directoryTenantId
            ResourceManagerEndpoint                  = $ResourceManagerEndpoint
            GalleryEndpoint                          = $endpoints.galleryEndpoint
            GraphEndpoint                            = $endpoints.graphEndpoint
            GraphAudience                            = $endpoints.graphEndpoint
        }

        $azureEnvironment = Add-AzureRmEnvironment @azureEnvironmentParams -ErrorAction Ignore
        $azureEnvironment = Get-AzureRmEnvironment -Name $environmentName -ErrorAction Stop

        return $azureEnvironment
    }

    function Initialize-AzureRmUserAccount([Microsoft.Azure.Commands.Profile.Models.PSAzureEnvironment]$azureEnvironment) {
        $params = @{
            EnvironmentName = $azureEnvironment.Name
            TenantId        = $azureEnvironment.AdTenant
        }

        if ($AutomationCredential) {
            $params += @{ Credential = $AutomationCredential }
        }

        # Prompts the user for interactive login flow if automation credential is not specified
        $azureAccount = Add-AzureRmAccount @params

        if ($SubscriptionName) {
            Select-AzureRmSubscription -SubscriptionName $SubscriptionName | Out-Null
        }
        elseif ($SubscriptionId) {
            Select-AzureRmSubscription -SubscriptionId $SubscriptionId | Out-Null
        }

        return $azureAccount
    }

    function Write-DecommissionImplicationsWarning {
        $params = @{
            Message       = ''
            WarningAction = 'Inquire'
        }
        $params.Message += 'You are removing a directory tenant from your Azure Stack deployment.'
        $params.Message += ' Users in this directory will be unable to access or manage any existing subscriptions (access to any existing resources may be impaired if they require identity integration).'
        $params.Message += " Additionally, you should first ensure that an Administrator of the directory '$directoryTenantName' has completed their decommissioning process before removing this access"
        $params.Message += ' (they will need to query your Azure Stack deployment to see which identities need to be removed from their directory).'

        if ($AutomationCredential) {
            $params.WarningAction = 'Continue'
        }
        else {
            $params.Message += " Would you like to proceed?"
        }

        Write-Warning @params
    }

    Invoke-Main
}

<#
.Synopsis
Removes the installed Azure Stack identity applications and their permissions within the callers's Azure Directory Tenant.
.DESCRIPTION
Removes the installed Azure Stack identity applications and their permissions within the callers's Azure Directory Tenant.
.EXAMPLE
$tenantARMEndpoint = "https://management.local.azurestack.external"
$myDirectoryTenantName = "<guestDirectoryTenant>.onmicrosoft.com"
 
Unregister-AzsWithMyDirectoryTenant -TenantResourceManagerEndpoint $tenantARMEndpoint `
    -DirectoryTenantName $myDirectoryTenantName -Verbose -Debug
#>


function Unregister-AzsWithMyDirectoryTenant {
    [CmdletBinding()]
    param
    (
        # The endpoint of the Azure Stack Resource Manager service.
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [ValidateScript( { $_.Scheme -eq [System.Uri]::UriSchemeHttps })]
        [uri] $TenantResourceManagerEndpoint,

        # The name of the directory tenant being onboarded.
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $DirectoryTenantName,

        # Optional: A credential used to authenticate with Azure Stack. Must support a non-interactive authentication flow. If not provided, the script will prompt for user credentials.
        [Parameter()]
        [ValidateNotNull()]
        [pscredential] $AutomationCredential = $null
    )

    $ErrorActionPreference = 'Stop'
    $VerbosePreference = 'Continue'

    $ResourceManagerEndpoint = $TenantResourceManagerEndpoint
    
    Import-Module 'AzureRm.Profile' -Verbose:$false 4> $null
    Import-Module "$PSScriptRoot\GraphAPI\GraphAPI.psm1" -Verbose:$false 4> $null
    
    function Invoke-Main {
        Write-DecommissionImplicationsWarning
    
        # Initialize the Azure PowerShell module to communicate with the Azure Resource Manager in the public cloud corresponding to the Azure Stack Graph Service. Will prompt user for credentials.
        Write-Host "Authenticating user..."
        $azureStackEnvironment = Initialize-AzureRmEnvironment 'AzureStack'
        $azureEnvironment = Resolve-AzureEnvironment $azureStackEnvironment
        $refreshToken = Initialize-AzureRmUserAccount $azureEnvironment $azureStackEnvironment.AdTenant
    
        # Initialize the Graph PowerShell module to communicate with the correct graph service
        $graphEnvironment = Resolve-GraphEnvironment $azureEnvironment
        Initialize-GraphEnvironment -Environment $graphEnvironment -DirectoryTenantId $DirectoryTenantName -RefreshToken $refreshToken
    
        # Call Azure Stack Resource Manager to retrieve the list of registered applications which need to be removed from the directory tenant
        Write-Host "Acquiring an access token to communicate with Resource Manager... (if you already decommissioned this directory you may get an error here which you can ignore)"
        $armAccessToken = (Get-GraphToken -Resource $azureStackEnvironment.ActiveDirectoryServiceEndpointResourceId -UseEnvironmentData -ErrorAction Stop).access_token
    
        Write-Host "Looking-up the registered identity applications which need to be uninstalled from your directory..."
        $applicationRegistrationParams = @{
            Method  = [Microsoft.PowerShell.Commands.WebRequestMethod]::Get
            Headers = @{ Authorization = "Bearer $armAccessToken" }
            Uri     = "$($ResourceManagerEndpoint.ToString().TrimEnd('/'))/applicationRegistrations?api-version=2014-04-01-preview"
        }
        $applicationRegistrations = Invoke-RestMethod @applicationRegistrationParams | Select -ExpandProperty value
    
        # Delete the service principals for the registered applications
        foreach ($applicationRegistration in $applicationRegistrations) {
            if (($applicationServicePrincipal = Get-GraphApplicationServicePrincipal -ApplicationId $applicationRegistration.appId -ErrorAction Continue)) {
                Write-Verbose "Uninstalling service principal: $(ConvertTo-Json $applicationServicePrincipal)" -Verbose
                Remove-GraphObject -objectId $applicationServicePrincipal.objectId
                Write-Host "Application '$($applicationServicePrincipal.appId)' ($($applicationServicePrincipal.appDisplayName)) was successfully uninstalled from your directory."
            }
            else {
                Write-Host "Application '$($applicationRegistration.appId)' is not installed or was already successfully uninstalled from your directory."
            }
        }
    
        Write-Host "All Azure Stack applications have been uninstalled! Your directory '$DirectoryTenantName' has been successfully decommissioned and can no-longer be used with Azure Stack."
    }
    
    function Initialize-AzureRmEnvironment([string]$environmentName) {
        $endpoints = Invoke-RestMethod -Method Get -Uri "$($ResourceManagerEndpoint.ToString().TrimEnd('/'))/metadata/endpoints?api-version=2015-01-01" -Verbose
        Write-Verbose -Message "Endpoints: $(ConvertTo-Json $endpoints)" -Verbose
    
        # resolve the directory tenant ID from the name
        $directoryTenantId = (New-Object uri(Invoke-RestMethod "$($endpoints.authentication.loginEndpoint.TrimEnd('/'))/$DirectoryTenantName/.well-known/openid-configuration").token_endpoint).AbsolutePath.Split('/')[1]
    
        $azureEnvironmentParams = @{
            Name                                     = $environmentName
            ActiveDirectoryEndpoint                  = $endpoints.authentication.loginEndpoint.TrimEnd('/') + "/"
            ActiveDirectoryServiceEndpointResourceId = $endpoints.authentication.audiences[0]
            AdTenant                                 = $directoryTenantId
            ResourceManagerEndpoint                  = $ResourceManagerEndpoint
            GalleryEndpoint                          = $endpoints.galleryEndpoint
            GraphEndpoint                            = $endpoints.graphEndpoint
            GraphAudience                            = $endpoints.graphEndpoint
        }
    
        $azureEnvironment = Add-AzureRmEnvironment @azureEnvironmentParams -ErrorAction Ignore
        $azureEnvironment = Get-AzureRmEnvironment -Name $environmentName -ErrorAction Stop
    
        return $azureEnvironment
    }
    
    function Resolve-AzureEnvironment([Microsoft.Azure.Commands.Profile.Models.PSAzureEnvironment]$azureStackEnvironment) {
        $azureEnvironment = Get-AzureRmEnvironment |
        Where GraphEndpointResourceId -EQ $azureStackEnvironment.GraphEndpointResourceId |
        Where Name -In @('AzureCloud', 'AzureChinaCloud', 'AzureUSGovernment', 'AzureGermanCloud')
    
        # Differentiate between AzureCloud and AzureUSGovernment
        if ($azureEnvironment.Count -ge 2) {
            $name = if ($azureStackEnvironment.ActiveDirectoryAuthority -eq 'https://login-us.microsoftonline.com/' -or $azureStackEnvironment.ActiveDirectoryAuthority -eq 'https://login.microsoftonline.us/') { 'AzureUSGovernment' } else { 'AzureCloud' }
            $azureEnvironment = $azureEnvironment | Where Name -EQ $name
        }
    
        return $azureEnvironment
    }
    
    function Initialize-AzureRmUserAccount([Microsoft.Azure.Commands.Profile.Models.PSAzureEnvironment]$azureEnvironment, [string]$directoryTenantId) {
        $params = @{
            EnvironmentName = $azureEnvironment.Name
            TenantId        = $directoryTenantId
        }
    
        if ($AutomationCredential) {
            $params += @{ Credential = $AutomationCredential }
        }
    
        # Prompts the user for interactive login flow if automation credential is not specified
        $azureAccount = Add-AzureRmAccount @params
    
        # Retrieve the refresh token
        $tokens = @()
        $tokens += try { [Microsoft.IdentityModel.Clients.ActiveDirectory.TokenCache]::DefaultShared.ReadItems() } catch { }
        $tokens += try { [Microsoft.Azure.Commands.Common.Authentication.AzureSession]::Instance.TokenCache.ReadItems() } catch { }
        $refreshToken = $tokens |
        Where Resource -EQ $azureEnvironment.ActiveDirectoryServiceEndpointResourceId |
        Where IsMultipleResourceRefreshToken -EQ $true |
        Where DisplayableId -EQ $azureAccount.Context.Account.Id |
        Sort ExpiresOn |
        Select -Last 1 -ExpandProperty RefreshToken |
        ConvertTo-SecureString -AsPlainText -Force

        # Workaround due to regression in AzurePowerShell profile module which fails to populate the response object of "Add-AzureRmAccount" cmdlet
        if (-not $refreshToken) {
            if ($tokens.Count -eq 1) {
                Write-Warning "Failed to find target refresh token from Azure PowerShell Cache; attempting to reuse the single cached auth context..."
                $refreshToken = $tokens[0].RefreshToken | ConvertTo-SecureString -AsPlainText -Force
            }
            else {
                throw "Unable to find refresh token from Azure PowerShell Cache. Please try the command again in a fresh PowerShell instance after running 'Clear-AzureRmContext -Scope CurrentUser -Force -Verbose'."
            }
        }

        return $refreshToken
    }
    
    function Resolve-GraphEnvironment([Microsoft.Azure.Commands.Profile.Models.PSAzureEnvironment]$azureEnvironment) {
        $graphEnvironment = switch ($azureEnvironment.ActiveDirectoryAuthority) {
            'https://login.microsoftonline.com/' { 'AzureCloud' }
            'https://login.chinacloudapi.cn/' { 'AzureChinaCloud' }
            'https://login-us.microsoftonline.com/' { 'AzureUSGovernment' }
            'https://login.microsoftonline.us/' { 'AzureUSGovernment' }
            'https://login.microsoftonline.de/' { 'AzureGermanCloud' }
    
            Default { throw "Unsupported graph resource identifier: $_" }
        }
    
        return $graphEnvironment
    }
    
    function Write-DecommissionImplicationsWarning {
        $params = @{
            Message       = ''
            WarningAction = 'Inquire'
        }
        $params.Message += 'You are removing access from an Azure Stack deployment to your directory tenant.'
        $params.Message += ' Users in your directory will be unable to access or manage any existing subscriptions in the Azure Stack deployment (access to any existing resources may be impaired if they require identity integration).'
    
        if ($AutomationCredential) {
            $params.WarningAction = 'Continue'
        }
        else {
            $params.Message += " Would you like to proceed?"
        }
    
        Write-Warning @params
    }
    
    $logFile = Join-Path -Path $PSScriptRoot -ChildPath "$DirectoryTenantName.$(Get-Date -Format MM-dd_HH-mm-ss_ms).log"
    Write-Verbose "Logging additional information to log file '$logFile'" -Verbose
    
    $logStartMessage = "[$(Get-Date -Format 'hh:mm:ss tt')] - Beginning invocation of '$($MyInvocation.InvocationName)' with parameters: $(ConvertTo-Json $PSBoundParameters -Depth 4)"
    $logStartMessage >> $logFile
    
    try {
        # Redirect verbose output to a log file
        Invoke-Main 4>> $logFile
    
        $logEndMessage = "[$(Get-Date -Format 'hh:mm:ss tt')] - Script completed successfully."
        $logEndMessage >> $logFile
    }
    catch {
        $logErrorMessage = "[$(Get-Date -Format 'hh:mm:ss tt')] - Script terminated with error: $_`r`n$($_.Exception)"
        $logErrorMessage >> $logFile
        Write-Warning "An error has occurred; more information may be found in the log file '$logFile'" -WarningAction Continue
        throw
    }
}

Export-ModuleMember -Function @(
    "Register-AzsGuestDirectoryTenant",
    "Update-AzsHomeDirectoryTenant",
    "Register-AzsWithMyDirectoryTenant",
    "Unregister-AzsGuestDirectoryTenant",
    "Unregister-AzsWithMyDirectoryTenant",
    "Get-AzsDirectoryTenantidentifier",
    "Get-AzsHealthReport",
    "New-AzsADGraphServicePrincipal"
)

# SIG # Begin signature block
# MIIjigYJKoZIhvcNAQcCoIIjezCCI3cCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBr1YgOr+0Pugvq
# BGlarFMxo8s0cMS4xOr/GLBBLkXhlKCCDYUwggYDMIID66ADAgECAhMzAAABUptA
# n1BWmXWIAAAAAAFSMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMTkwNTAyMjEzNzQ2WhcNMjAwNTAyMjEzNzQ2WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQCxp4nT9qfu9O10iJyewYXHlN+WEh79Noor9nhM6enUNbCbhX9vS+8c/3eIVazS
# YnVBTqLzW7xWN1bCcItDbsEzKEE2BswSun7J9xCaLwcGHKFr+qWUlz7hh9RcmjYS
# kOGNybOfrgj3sm0DStoK8ljwEyUVeRfMHx9E/7Ca/OEq2cXBT3L0fVnlEkfal310
# EFCLDo2BrE35NGRjG+/nnZiqKqEh5lWNk33JV8/I0fIcUKrLEmUGrv0CgC7w2cjm
# bBhBIJ+0KzSnSWingXol/3iUdBBy4QQNH767kYGunJeY08RjHMIgjJCdAoEM+2mX
# v1phaV7j+M3dNzZ/cdsz3oDfAgMBAAGjggGCMIIBfjAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQU3f8Aw1sW72WcJ2bo/QSYGzVrRYcw
# VAYDVR0RBE0wS6RJMEcxLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJh
# dGlvbnMgTGltaXRlZDEWMBQGA1UEBRMNMjMwMDEyKzQ1NDEzNjAfBgNVHSMEGDAW
# gBRIbmTlUAXTgqoXNzcitW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8v
# d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIw
# MTEtMDctMDguY3JsMGEGCCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDov
# L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDEx
# XzIwMTEtMDctMDguY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIB
# AJTwROaHvogXgixWjyjvLfiRgqI2QK8GoG23eqAgNjX7V/WdUWBbs0aIC3k49cd0
# zdq+JJImixcX6UOTpz2LZPFSh23l0/Mo35wG7JXUxgO0U+5drbQht5xoMl1n7/TQ
# 4iKcmAYSAPxTq5lFnoV2+fAeljVA7O43szjs7LR09D0wFHwzZco/iE8Hlakl23ZT
# 7FnB5AfU2hwfv87y3q3a5qFiugSykILpK0/vqnlEVB0KAdQVzYULQ/U4eFEjnis3
# Js9UrAvtIhIs26445Rj3UP6U4GgOjgQonlRA+mDlsh78wFSGbASIvK+fkONUhvj8
# B8ZHNn4TFfnct+a0ZueY4f6aRPxr8beNSUKn7QW/FQmn422bE7KfnqWncsH7vbNh
# G929prVHPsaa7J22i9wyHj7m0oATXJ+YjfyoEAtd5/NyIYaE4Uu0j1EhuYUo5VaJ
# JnMaTER0qX8+/YZRWrFN/heps41XNVjiAawpbAa0fUa3R9RNBjPiBnM0gvNPorM4
# dsV2VJ8GluIQOrJlOvuCrOYDGirGnadOmQ21wPBoGFCWpK56PxzliKsy5NNmAXcE
# x7Qb9vUjY1WlYtrdwOXTpxN4slzIht69BaZlLIjLVWwqIfuNrhHKNDM9K+v7vgrI
# bf7l5/665g0gjQCDCN6Q5sxuttTAEKtJeS/pkpI+DbZ/MIIHejCCBWKgAwIBAgIK
# YQ6Q0gAAAAAAAzANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNV
# BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv
# c29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlm
# aWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEw
# OTA5WjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYD
# VQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG
# 9w0BAQEFAAOCAg8AMIICCgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+la
# UKq4BjgaBEm6f8MMHt03a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc
# 6Whe0t+bU7IKLMOv2akrrnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4D
# dato88tt8zpcoRb0RrrgOGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+
# lD3v++MrWhAfTVYoonpy4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nk
# kDstrjNYxbc+/jLTswM9sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6
# A4aN91/w0FK/jJSHvMAhdCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmd
# X4jiJV3TIUs+UsS1Vz8kA/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL
# 5zmhD+kjSbwYuER8ReTBw3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zd
# sGbiwZeBe+3W7UvnSSmnEyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3
# T8HhhUSJxAlMxdSlQy90lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS
# 4NaIjAsCAwEAAaOCAe0wggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRI
# bmTlUAXTgqoXNzcitW2oynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTAL
# BgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBD
# uRQFTuHqp8cx0SOJNDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jv
# c29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf
# MDNfMjIuY3JsMF4GCCsGAQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf
# MDNfMjIuY3J0MIGfBgNVHSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEF
# BQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1h
# cnljcHMuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkA
# YwB5AF8AcwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn
# 8oalmOBUeRou09h0ZyKbC5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7
# v0epo/Np22O/IjWll11lhJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0b
# pdS1HXeUOeLpZMlEPXh6I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/
# KmtYSWMfCWluWpiW5IP0wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvy
# CInWH8MyGOLwxS3OW560STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBp
# mLJZiWhub6e3dMNABQamASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJi
# hsMdYzaXht/a8/jyFqGaJ+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYb
# BL7fQccOKO7eZS/sl/ahXJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbS
# oqKfenoi+kiVH6v7RyOA9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sL
# gOppO6/8MO0ETI7f33VtY5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtX
# cVZOSEXAQsmbdlsKgEhr/Xmfwb1tbWrJUnMTDXpQzTGCFVswghVXAgEBMIGVMH4x
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01p
# Y3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTECEzMAAAFSm0CfUFaZdYgAAAAA
# AVIwDQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQw
# HAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIJ9v
# Uhr43qblvzlED5uOltoum3cA3g7ii5GjCLLcabvEMEIGCisGAQQBgjcCAQwxNDAy
# oBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20wDQYJKoZIhvcNAQEBBQAEggEACRqBV93Qp3Ai41wrvnig+Tky/oilfjdIipXM
# y4Z7dQyNBgg67dYtpFC//OsTbNtwSGlTs3vPtr0NhU3JfpW+/07agLiithkiIIdD
# EZAiDd0EdPyW/aKlxzPA3jRzNxL80EbAYvNdHbW7B026IvlHgUuvDGhP6HjMoqiG
# JQZ/Hs/Rq125oReDxc4Zou+bubwY4VsZqBCkQpK5LcfvHEejDIGB3XijDnaaoHgc
# EfX24Qyp/LYRc++houezWpJ1/YEP1Yv2fD//8oB1IdGoIZvV3mXL2YoGFLdtNXD5
# l66TmN/qf1J5hmeBftabMXp9cn6n0RGiFc8805uqt4ylFAny9KGCEuUwghLhBgor
# BgEEAYI3AwMBMYIS0TCCEs0GCSqGSIb3DQEHAqCCEr4wghK6AgEDMQ8wDQYJYIZI
# AWUDBAIBBQAwggFRBgsqhkiG9w0BCRABBKCCAUAEggE8MIIBOAIBAQYKKwYBBAGE
# WQoDATAxMA0GCWCGSAFlAwQCAQUABCBSiY3updav9xmcFbWPHkh0EAzjvX/fn2LS
# VfnVtp5RFQIGXfvAsABHGBMyMDIwMDExNTA0MjU1My4zNzZaMASAAgH0oIHQpIHN
# MIHKMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQL
# ExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMSYwJAYDVQQLEx1UaGFsZXMg
# VFNTIEVTTjpBRTJDLUUzMkItMUFGQzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUt
# U3RhbXAgU2VydmljZaCCDjwwggTxMIID2aADAgECAhMzAAABFpMi6r+7LU3mAAAA
# AAEWMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo
# aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y
# cG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEw
# MB4XDTE5MTExMzIxNDAzNFoXDTIxMDIxMTIxNDAzNFowgcoxCzAJBgNVBAYTAlVT
# MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJTAjBgNVBAsTHE1pY3Jvc29mdCBBbWVy
# aWNhIE9wZXJhdGlvbnMxJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNOOkFFMkMtRTMy
# Qi0xQUZDMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNlMIIB
# IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Pgb8/296ie/Lj2rWq+MIlMZ
# wkSUwZsIKd472tyeVOyNcKgqSCT4zQvz2kd+VD7lYWN3V0USL5oipdp+xp7wH7CA
# HC7zNU21PjdHWPOi2okIlPyTikrQBowo+MOV9Xgd3WqMnJSKEank7QmSHgJimJ2q
# /ZRR5+0Z5uZRejJHkQcJmTB8Gq/wg2E/gjuRl/iGa4fGJu0cHSUiX78m5FEyaac1
# XnkqafSqYR8qb7sn3ZVt/ltbiGUJr874oi2bZduUtCMR0QiWWfBMExcLV4A6ermC
# 98cbbvi/pQb1p1l7vXT2NReD+xkFqzKn0cA3Vi9cc5LjDhY91L18RuHIgU3qHQID
# AQABo4IBGzCCARcwHQYDVR0OBBYEFOW/Xiu4F+gXzUflH3k0/lfIIVULMB8GA1Ud
# IwQYMBaAFNVjOlyKMZDzQ3t8RhvFM2hahW1VMFYGA1UdHwRPME0wS6BJoEeGRWh0
# dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1RpbVN0
# YVBDQV8yMDEwLTA3LTAxLmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKG
# Pmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljVGltU3RhUENB
# XzIwMTAtMDctMDEuY3J0MAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYIKwYBBQUH
# AwgwDQYJKoZIhvcNAQELBQADggEBADaDatfaqaPbAy/pSdK8e8XdzN6v9979NSWL
# UsNHoNBFpyr1FTGcvwf0SKIfe0ygt8s8plkAYxMUftUmOnO+OnGXUgTOreXIw4zt
# sepotreHcL094+bn7OUGLPMa56GQii3WUgiGPP0gfNXhXcqSdd9HmXjMhKfRn0jO
# KREJTPqPHLXSxcA1SVTrg8JDtkD+yWVzuuAkSopTGxtJp5PcrYUrMb7nW1coIe7t
# sQiSPp6xFVzKfXFUJ9VzAChucE+8pqXLpV/xU3p/1vf0DgLZMpI22mwAgbe/E6wg
# yDSKyHXI4UsiIlSYASv+IlKOtcXzrXV0IRQUdRyIC1ZiWWL/YggwggZxMIIEWaAD
# AgECAgphCYEqAAAAAAACMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzET
# MBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV
# TWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBD
# ZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxMDAeFw0xMDA3MDEyMTM2NTVaFw0yNTA3
# MDEyMTQ2NTVaMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# JjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMIIBIjANBgkq
# hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqR0NvHcRijog7PwTl/X6f2mUa3RUENWl
# CgCChfvtfGhLLF/Fw+Vhwna3PmYrW/AVUycEMR9BGxqVHc4JE458YTBZsTBED/Fg
# iIRUQwzXTbg4CLNC3ZOs1nMwVyaCo0UN0Or1R4HNvyRgMlhgRvJYR4YyhB50YWeR
# X4FUsc+TTJLBxKZd0WETbijGGvmGgLvfYfxGwScdJGcSchohiq9LZIlQYrFd/Xcf
# PfBXday9ikJNQFHRD5wGPmd/9WbAA5ZEfu/QS/1u5ZrKsajyeioKMfDaTgaRtogI
# Neh4HLDpmc085y9Euqf03GS9pAHBIAmTeM38vMDJRF1eFpwBBU8iTQIDAQABo4IB
# 5jCCAeIwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFNVjOlyKMZDzQ3t8RhvF
# M2hahW1VMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1UdDwQEAwIBhjAP
# BgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNX2VsuP6KJcYmjRPZSQW9fOmhjE
# MFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kv
# Y3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNybDBaBggrBgEF
# BQcBAQROMEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9w
# a2kvY2VydHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3J0MIGgBgNVHSABAf8E
# gZUwgZIwgY8GCSsGAQQBgjcuAzCBgTA9BggrBgEFBQcCARYxaHR0cDovL3d3dy5t
# aWNyb3NvZnQuY29tL1BLSS9kb2NzL0NQUy9kZWZhdWx0Lmh0bTBABggrBgEFBQcC
# AjA0HjIgHQBMAGUAZwBhAGwAXwBQAG8AbABpAGMAeQBfAFMAdABhAHQAZQBtAGUA
# bgB0AC4gHTANBgkqhkiG9w0BAQsFAAOCAgEAB+aIUQ3ixuCYP4FxAz2do6Ehb7Pr
# psz1Mb7PBeKp/vpXbRkws8LFZslq3/Xn8Hi9x6ieJeP5vO1rVFcIK1GCRBL7uVOM
# zPRgEop2zEBAQZvcXBf/XPleFzWYJFZLdO9CEMivv3/Gf/I3fVo/HPKZeUqRUgCv
# OA8X9S95gWXZqbVr5MfO9sp6AG9LMEQkIjzP7QOllo9ZKby2/QThcJ8ySif9Va8v
# /rbljjO7Yl+a21dA6fHOmWaQjP9qYn/dxUoLkSbiOewZSnFjnXshbcOco6I8+n99
# lmqQeKZt0uGc+R38ONiU9MalCpaGpL2eGq4EQoO4tYCbIjggtSXlZOz39L9+Y1kl
# D3ouOVd2onGqBooPiRa6YacRy5rYDkeagMXQzafQ732D8OE7cQnfXXSYIghh2rBQ
# Hm+98eEA3+cxB6STOvdlR3jo+KhIq/fecn5ha293qYHLpwmsObvsxsvYgrRyzR30
# uIUBHoD7G4kqVDmyW9rIDVWZeodzOwjmmC3qjeAzLhIp9cAvVCch98isTtoouLGp
# 25ayp0Kiyc8ZQU3ghvkqmqMRZjDTu3QyS99je/WZii8bxyGvWbWu3EQ8l1Bx16HS
# xVXjad5XwdHeMMD9zOZN+w2/XU/pnR4ZOC+8z1gFLu8NoFA12u8JJxzVs341Hgi6
# 2jbb01+P3nSISRKhggLOMIICNwIBATCB+KGB0KSBzTCByjELMAkGA1UEBhMCVVMx
# EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT
# FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJp
# Y2EgT3BlcmF0aW9uczEmMCQGA1UECxMdVGhhbGVzIFRTUyBFU046QUUyQy1FMzJC
# LTFBRkMxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2WiIwoB
# ATAHBgUrDgMCGgMVAIdNW9zyT6CLG1qCDNc++szs3ZZDoIGDMIGApH4wfDELMAkG
# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9z
# b2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwDQYJKoZIhvcNAQEFBQACBQDhyIXdMCIY
# DzIwMjAwMTE1MDIyNDI5WhgPMjAyMDAxMTYwMjI0MjlaMHcwPQYKKwYBBAGEWQoE
# ATEvMC0wCgIFAOHIhd0CAQAwCgIBAAICEloCAf8wBwIBAAICEawwCgIFAOHJ110C
# AQAwNgYKKwYBBAGEWQoEAjEoMCYwDAYKKwYBBAGEWQoDAqAKMAgCAQACAwehIKEK
# MAgCAQACAwGGoDANBgkqhkiG9w0BAQUFAAOBgQCO5r3nsSDHXK894vG+O5Tn7Rtd
# AfUM+WpM5E75XIHAGsGCtUUruqRcVTaccuxPaeDtuxV8MGEtmvngL3xq0DGb3JOK
# U6NosFNtLtWCaKopICHql3A/cC/8zayhJFYmOAf0nnYw4E2srr2/g99u0BTz5drA
# C7wgoNqXaJO7/1BIcTGCAw0wggMJAgEBMIGTMHwxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w
# IFBDQSAyMDEwAhMzAAABFpMi6r+7LU3mAAAAAAEWMA0GCWCGSAFlAwQCAQUAoIIB
# SjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwLwYJKoZIhvcNAQkEMSIEINQW
# FFyxphVKp+w6hQzItapRLtZjYn+a8i8EoIK3TSycMIH6BgsqhkiG9w0BCRACLzGB
# 6jCB5zCB5DCBvQQggyKU9qRgKQiXXCmbITbdtLENhYxqIMhBaM+iXtLBkMowgZgw
# gYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD
# VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAARaTIuq/uy1N
# 5gAAAAABFjAiBCCF8sHaGNf2KMtXfv1tP0Yy8KTlq03Wm33gKv593WI1NDANBgkq
# hkiG9w0BAQsFAASCAQCjYrp3MVGAi5RdKh5/EN8BmK5QaY+D22ETwNjMhrJelxy8
# TaVOj/ttdjeHQQppsMqdW9ZuDVrkS+yTjMXxAqX+mUDoXtJMbeEuhOYGEFguxZCp
# WYaZjDRoH0J6nVzqp6394EhUIXfu/6PJq6wMuF7hd62n0/z68kwNh5zCITaGnlIv
# nIOVfQjo3OvFdhkzRKsJemubfp+piPFwyg2sEquwK6rxvrbLsKdBKzrTOdSBeLVg
# wy2KTtWhiCuwWwS7rYQlqtNd+JGVC5RhYPPksEv5PIq5v4yKHcKA/k1LoQD7QvMq
# VfXmX/dj+LSm29oR1KnVYzJAYMKksRdB0x3myIJc
# SIG # End signature block