@@ -17,7 +17,7 @@ function Get-AzsDirectoryTenantidentifier {
1717 [CmdletBinding ()]
1818 Param
1919 (
20- # Param1 help description
20+ # The Authority of the identity system, e.g. "https://login.windows.net/microsoft.onmicrosoft.com"
2121 [Parameter (Mandatory = $true ,
2222 Position = 0 )]
2323 $Authority
@@ -214,6 +214,300 @@ function Register-AzsGuestDirectoryTenant {
214214 Invoke-Main
215215}
216216
217+ <#
218+ . Synopsis
219+ Consents to any missing permissions for Azure Stack identity applications in the home directory of Azure Stack.
220+ . DESCRIPTION
221+ 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.
222+ . EXAMPLE
223+ $adminResourceManagerEndpoint = "https://adminmanagement.local.azurestack.external"
224+ $homeDirectoryTenantName = "<homeDirectoryTenant>.onmicrosoft.com"
225+
226+ Update-AzsHomeDirectoryTenant -AdminResourceManagerEndpoint $adminResourceManagerEndpoint `
227+ -DirectoryTenantName $homeDirectoryTenantName -Verbose
228+ #>
229+
230+ function Update-AzsHomeDirectoryTenant {
231+ [CmdletBinding ()]
232+ param
233+ (
234+ # The endpoint of the Azure Stack Resource Manager service.
235+ [Parameter (Mandatory = $true )]
236+ [ValidateNotNull ()]
237+ [ValidateScript ( {$_.Scheme -eq [System.Uri ]::UriSchemeHttps})]
238+ [uri ] $AdminResourceManagerEndpoint ,
239+
240+ # The name of the home Directory Tenant in which the Azure Stack Administrator subscription resides.
241+ [Parameter (Mandatory = $true )]
242+ [ValidateNotNullOrEmpty ()]
243+ [string ] $DirectoryTenantName ,
244+
245+ # 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.
246+ [Parameter ()]
247+ [ValidateNotNull ()]
248+ [pscredential ] $AutomationCredential = $null
249+ )
250+
251+ $ErrorActionPreference = ' Stop'
252+ $VerbosePreference = ' Continue'
253+
254+ # Install-Module AzureRm
255+ Import-Module ' AzureRm.Profile' - Verbose:$false 4> $null
256+ Import-Module " $PSScriptRoot \GraphAPI\GraphAPI.psm1" - Verbose:$false 4> $null
257+
258+ function Invoke-Main {
259+ # 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.
260+ Write-Host " Authenticating user..."
261+ $azureStackEnvironment = Initialize-AzureRmEnvironment ' AzureStackAdmin'
262+ $refreshToken = Initialize-AzureRmUserAccount $azureStackEnvironment
263+
264+ # Initialize the Graph PowerShell module to communicate with the correct graph service
265+ $graphEnvironment = Resolve-GraphEnvironment $azureStackEnvironment
266+ Initialize-GraphEnvironment - Environment $graphEnvironment - DirectoryTenantId $DirectoryTenantName - RefreshToken $refreshToken
267+
268+ # Call Azure Stack Resource Manager to retrieve the list of registered applications which need to be initialized in the onboarding directory tenant
269+ Write-Host " Acquiring an access token to communicate with Resource Manager..."
270+ $armAccessToken = Get-ArmAccessToken $azureStackEnvironment
271+
272+ Write-Host " Looking-up the registered identity applications which need to be installed in your directory..."
273+ $applicationRegistrationParams = @ {
274+ Method = [Microsoft.PowerShell.Commands.WebRequestMethod ]::Get
275+ Headers = @ { Authorization = " Bearer $armAccessToken " }
276+ Uri = " $ ( $AdminResourceManagerEndpoint.ToString ().TrimEnd(' /' )) /applicationRegistrations?api-version=2014-04-01-preview"
277+ }
278+ $applicationRegistrations = Invoke-RestMethod @applicationRegistrationParams | Select - ExpandProperty value
279+
280+ # Identify which permissions have already been granted to each registered application and which additional permissions need to be granted
281+ $permissions = @ ()
282+ $count = 0
283+ foreach ($applicationRegistration in $applicationRegistrations ) {
284+ # Initialize the service principal for the registered application
285+ $count ++
286+ $applicationServicePrincipal = Initialize-GraphApplicationServicePrincipal - ApplicationId $applicationRegistration.appId
287+ Write-Host " Installing Application... ($ ( $count ) of $ ( $applicationRegistrations.Count ) ): $ ( $applicationServicePrincipal.appId ) '$ ( $applicationServicePrincipal.appDisplayName ) '"
288+
289+ # WORKAROUND - the recent Azure Stack update has a missing permission registration; temporarily "inject" this permission registration into the returned data
290+ if ($applicationServicePrincipal.servicePrincipalNames | Where { $_ -like ' https://deploymentprovider.*/*' })
291+ {
292+ Write-Verbose " Adding missing permission registrations for application '$ ( $applicationServicePrincipal.appDisplayName ) ' ($ ( $applicationServicePrincipal.appId ) )..." - Verbose
293+
294+ $graph = Get-GraphApplicationServicePrincipal - ApplicationId (Get-GraphEnvironmentInfo ).Applications.WindowsAzureActiveDirectory.Id
295+
296+ $applicationRegistration.appRoleAssignments = @ (
297+ [pscustomobject ]@ {
298+ resource = (Get-GraphEnvironmentInfo ).Applications.WindowsAzureActiveDirectory.Id
299+ client = $applicationRegistration.appId
300+ roleId = $graph.appRoles | Where value -EQ ' Directory.Read.All' | Select - ExpandProperty id
301+ },
302+
303+ [pscustomobject ]@ {
304+ resource = (Get-GraphEnvironmentInfo ).Applications.WindowsAzureActiveDirectory.Id
305+ client = $applicationRegistration.appId
306+ roleId = $graph.appRoles | Where value -EQ ' Application.ReadWrite.OwnedBy' | Select - ExpandProperty id
307+ }
308+ )
309+ }
310+
311+ # Initialize the necessary tags for the registered application
312+ if ($applicationRegistration.tags ) {
313+ Update-GraphApplicationServicePrincipalTags - ApplicationId $applicationRegistration.appId - Tags $applicationRegistration.tags
314+ }
315+
316+ # Lookup the permission consent status for the *application* permissions (either to or from) which the registered application requires
317+ foreach ($appRoleAssignment in $applicationRegistration.appRoleAssignments ) {
318+ $params = @ {
319+ ClientApplicationId = $appRoleAssignment.client
320+ ResourceApplicationId = $appRoleAssignment.resource
321+ PermissionType = ' Application'
322+ PermissionId = $appRoleAssignment.roleId
323+ }
324+ $permissions += New-GraphPermissionDescription @params - LookupConsentStatus
325+ }
326+
327+ # Lookup the permission consent status for the *delegated* permissions (either to or from) which the registered application requires
328+ foreach ($oauth2PermissionGrant in $applicationRegistration.oauth2PermissionGrants ) {
329+ $resourceApplicationServicePrincipal = Initialize-GraphApplicationServicePrincipal - ApplicationId $oauth2PermissionGrant.resource
330+ foreach ($scope in $oauth2PermissionGrant.scope.Split (' ' )) {
331+ $params = @ {
332+ ClientApplicationId = $oauth2PermissionGrant.client
333+ ResourceApplicationServicePrincipal = $resourceApplicationServicePrincipal
334+ PermissionType = ' Delegated'
335+ PermissionId = ($resourceApplicationServicePrincipal.oauth2Permissions | Where value -EQ $scope ).id
336+ }
337+ $permissions += New-GraphPermissionDescription @params - LookupConsentStatus
338+ }
339+ }
340+ }
341+
342+ # Trace the permission status
343+ Write-Verbose " Current permission status: $ ( $permissions | ConvertTo-Json - Depth 4 ) " - Verbose
344+
345+ $permissionFile = Join-Path - Path $PSScriptRoot - ChildPath " $DirectoryTenantName .permissions.json"
346+ $permissionContent = $permissions | Select - Property * - ExcludeProperty isConsented | ConvertTo-Json - Depth 4 | Out-String
347+ $permissionContent > $permissionFile
348+
349+ # Display application status to user
350+ $permissionsByClient = $permissions | Select * , @ {n = ' Client' ; e = {' {0} {1}' -f $_.clientApplicationId , $_.clientApplicationDisplayName }} | Sort clientApplicationDisplayName | Group Client
351+ $readyApplications = @ ()
352+ $pendingApplications = @ ()
353+ foreach ($client in $permissionsByClient ) {
354+ if ($client.Group.isConsented -Contains $false ) {
355+ $pendingApplications += $client
356+ }
357+ else {
358+ $readyApplications += $client
359+ }
360+ }
361+
362+ Write-Host " "
363+ if ($readyApplications ) {
364+ Write-Host " Applications installed and configured:"
365+ Write-Host " `t $ ( $readyApplications.Name -join " `r`n`t " ) "
366+ }
367+ if ($readyApplications -and $pendingApplications ) {
368+ Write-Host " "
369+ }
370+ if ($pendingApplications ) {
371+ Write-Host " Applications waiting to be configured:"
372+ Write-Host " `t $ ( $pendingApplications.Name -join " `r`n`t " ) "
373+ }
374+ Write-Host " "
375+
376+ # Grant any missing permissions for registered applications
377+ if ($permissions | Where isConsented -EQ $false | Select - First 1 ) {
378+ Write-Host " Configuring applications... (this may take up to a few minutes to complete)"
379+ Write-Host " "
380+ $permissions | Where isConsented -EQ $false | Grant-GraphApplicationPermission
381+ }
382+
383+ Write-Host " All applications installed and configured! Your home directory '$DirectoryTenantName ' has been successfully updated to be used with Azure Stack!"
384+ Write-Host " "
385+ 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 '."
386+ Write-Host " Run this script again at any time to check the status of the Azure Stack applications in your directory."
387+ Write-Warning " If your Azure Stack Administrator installs new services or updates in the future, you may need to run this script again."
388+ }
389+
390+ function Initialize-AzureRmEnvironment ([string ]$environmentName ) {
391+ $endpoints = Invoke-RestMethod - Method Get - Uri " $ ( $AdminResourceManagerEndpoint.ToString ().TrimEnd(' /' )) /metadata/endpoints?api-version=2015-01-01" - Verbose
392+ Write-Verbose - Message " Endpoints: $ ( ConvertTo-Json $endpoints ) " - Verbose
393+
394+ # resolve the directory tenant ID from the name
395+ $directoryTenantId = (New-Object uri(Invoke-RestMethod " $ ( $endpoints.authentication.loginEndpoint.TrimEnd (' /' )) /$DirectoryTenantName /.well-known/openid-configuration" ).token_endpoint).AbsolutePath.Split(' /' )[1 ]
396+
397+ $azureEnvironmentParams = @ {
398+ Name = $environmentName
399+ ActiveDirectoryEndpoint = $endpoints.authentication.loginEndpoint.TrimEnd (' /' ) + " /"
400+ ActiveDirectoryServiceEndpointResourceId = $endpoints.authentication.audiences [0 ]
401+ AdTenant = $directoryTenantId
402+ ResourceManagerEndpoint = $AdminResourceManagerEndpoint
403+ GalleryEndpoint = $endpoints.galleryEndpoint
404+ GraphEndpoint = $endpoints.graphEndpoint
405+ GraphAudience = $endpoints.graphEndpoint
406+ }
407+
408+ $azureEnvironment = Add-AzureRmEnvironment @azureEnvironmentParams - ErrorAction Ignore
409+ $azureEnvironment = Get-AzureRmEnvironment - Name $environmentName - ErrorAction Stop
410+
411+ return $azureEnvironment
412+ }
413+
414+ function Initialize-AzureRmUserAccount ([Microsoft.Azure.Commands.Profile.Models.PSAzureEnvironment ]$azureStackEnvironment ) {
415+ $params = @ {
416+ EnvironmentName = $azureStackEnvironment.Name
417+ TenantId = $azureStackEnvironment.AdTenant
418+ }
419+
420+ if ($AutomationCredential ) {
421+ $params += @ { Credential = $AutomationCredential }
422+ }
423+
424+ # Prompts the user for interactive login flow if automation credential is not specified
425+ $azureStackAccount = Add-AzureRmAccount @params
426+
427+ # Retrieve the refresh token
428+ $tokens = @ ()
429+ $tokens += try { [Microsoft.IdentityModel.Clients.ActiveDirectory.TokenCache ]::DefaultShared.ReadItems() } catch {}
430+ $tokens += try { [Microsoft.Azure.Commands.Common.Authentication.AzureSession ]::Instance.TokenCache.ReadItems() } catch {}
431+ $refreshToken = $tokens |
432+ Where Resource -EQ $azureStackEnvironment.ActiveDirectoryServiceEndpointResourceId |
433+ Where IsMultipleResourceRefreshToken -EQ $true |
434+ Where DisplayableId -EQ $azureStackAccount.Context.Account.Id |
435+ Sort ExpiresOn |
436+ Select - Last 1 - ExpandProperty RefreshToken |
437+ ConvertTo-SecureString - AsPlainText - Force
438+
439+ # Workaround due to regression in AzurePowerShell profile module which fails to populate the response object of "Add-AzureRmAccount" cmdlet
440+ if (-not $refreshToken ) {
441+ if ($tokens.Count -eq 1 ) {
442+ Write-Warning " Failed to find target refresh token from Azure PowerShell Cache; attempting to reuse the single cached auth context..."
443+ $refreshToken = $tokens [0 ].RefreshToken | ConvertTo-SecureString - AsPlainText - Force
444+ }
445+ else {
446+ 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'."
447+ }
448+ }
449+
450+ return $refreshToken
451+ }
452+
453+ function Resolve-GraphEnvironment ([Microsoft.Azure.Commands.Profile.Models.PSAzureEnvironment ]$azureEnvironment ) {
454+ $graphEnvironment = switch ($azureEnvironment.ActiveDirectoryAuthority ) {
455+ ' https://login.microsoftonline.com/' { ' AzureCloud' }
456+ ' https://login.chinacloudapi.cn/' { ' AzureChinaCloud' }
457+ ' https://login-us.microsoftonline.com/' { ' AzureUSGovernment' }
458+ ' https://login.microsoftonline.de/' { ' AzureGermanCloud' }
459+
460+ Default { throw " Unsupported graph resource identifier: $_ " }
461+ }
462+
463+ return $graphEnvironment
464+ }
465+
466+ function Get-ArmAccessToken ([Microsoft.Azure.Commands.Profile.Models.PSAzureEnvironment ]$azureStackEnvironment ) {
467+ $armAccessToken = $null
468+ $attempts = 0
469+ $maxAttempts = 12
470+ $delayInSeconds = 5
471+ do {
472+ try {
473+ $attempts ++
474+ $armAccessToken = (Get-GraphToken - Resource $azureStackEnvironment.ActiveDirectoryServiceEndpointResourceId - UseEnvironmentData - ErrorAction Stop).access_token
475+ }
476+ catch {
477+ if ($attempts -ge $maxAttempts ) {
478+ throw
479+ }
480+ Write-Verbose " Error attempting to acquire ARM access token: $_ `r`n $ ( $_.Exception ) " - Verbose
481+ Write-Verbose " Delaying for $delayInSeconds seconds before trying again... (attempt $attempts /$maxAttempts )" - Verbose
482+ Start-Sleep - Seconds $delayInSeconds
483+ }
484+ }
485+ while (-not $armAccessToken )
486+
487+ return $armAccessToken
488+ }
489+
490+ $logFile = Join-Path - Path $PSScriptRoot - ChildPath " $DirectoryTenantName .$ ( Get-Date - Format MM- dd_HH- mm- ss_ms) .log"
491+ Write-Verbose " Logging additional information to log file '$logFile '" - Verbose
492+
493+ $logStartMessage = " [$ ( Get-Date - Format ' hh:mm:ss tt' ) ] - Beginning invocation of '$ ( $MyInvocation.InvocationName ) ' with parameters: $ ( ConvertTo-Json $PSBoundParameters - Depth 4 ) "
494+ $logStartMessage >> $logFile
495+
496+ try {
497+ # Redirect verbose output to a log file
498+ Invoke-Main 4>> $logFile
499+
500+ $logEndMessage = " [$ ( Get-Date - Format ' hh:mm:ss tt' ) ] - Script completed successfully."
501+ $logEndMessage >> $logFile
502+ }
503+ catch {
504+ $logErrorMessage = " [$ ( Get-Date - Format ' hh:mm:ss tt' ) ] - Script terminated with error: $_ `r`n $ ( $_.Exception ) "
505+ $logErrorMessage >> $logFile
506+ Write-Warning " An error has occurred; more information may be found in the log file '$logFile '" - WarningAction Continue
507+ throw
508+ }
509+ }
510+
217511<#
218512. Synopsis
219513Consents to the given Azure Stack instance within the callers's Azure Directory Tenant.
@@ -882,6 +1176,7 @@ function Unregister-AzsWithMyDirectoryTenant {
8821176
8831177Export-ModuleMember - Function @ (
8841178 " Register-AzsGuestDirectoryTenant" ,
1179+ " Update-AzsHomeDirectoryTenant" ,
8851180 " Register-AzsWithMyDirectoryTenant" ,
8861181 " Unregister-AzsGuestDirectoryTenant" ,
8871182 " Unregister-AzsWithMyDirectoryTenant" ,
0 commit comments