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 |