public/Connect-MsecAzure.ps1
|
function Connect-MsecAzure { <# .SYNOPSIS Signs in to the Azure plane as YOU, for the commands that read ARM rather than Graph. .DESCRIPTION The module has three ways in and this is the third. Connect-Msec reads as the application, Connect-MsecAdmin writes as you through Graph, and the Azure-plane commands - Search-MsecLogAnalytics, Get-MsecAzureSecureScore, Get-MsecKeyVaultCertificate and the rest - run on the Az context, which is neither. Until now they told you to go and run Connect-AzAccount, a cmdlet whose defaults disagree with this module's. IT DEFAULTS TO -Scope Process, WHERE Connect-AzAccount DEFAULTS TO CurrentUser. That is the whole reason this exists. Az's context autosave writes the active context to ~/.Azure/AzureRmContext.json, so a sign-in in one shell silently becomes the default for every shell you open afterwards - you discover it later, in a different window, pointed at a tenant you do not remember choosing. Here the sign-in belongs to the shell that did it, and -Scope CurrentUser is how you ask for the old behaviour. THE CREDENTIAL IS SHARED WHATEVER THE SCOPE, and -Scope does not change that. The token goes into the OS credential store - on macOS the keychain, under the service name Microsoft.Developer.IdentityService - which is per user, not per process. What -Scope controls is the POINTER: which tenant and subscription a shell is aimed at. That is the part that decides where a command lands, and the part worth keeping local. ON macOS THE BROKER BREAKS INTERACTIVE SIGN-IN, and the error does not say so. Az 5.x enables the WAM broker by default (EnableLoginByWam), and it must run on the main thread, which the PowerShell host does not give it. The failure reads "InteractiveBrowserCredential authentication failed: Interactive requests with mac broker enabled must be executed on the main thread on macOS" and nothing about it suggests a configuration setting is at fault. A sign-in that fails this way is caught here and re-thrown naming the fix, and -UseDeviceCode gets you in meanwhile. A SOVEREIGN CLOUD IS SLOW, AND THE SLOW PART IS AFTER THE BROWSER. Measured on one machine: ~0.08 s per ARM round trip on AzureCloud against ~2.3 s on AzureChinaCloud. The sign-in itself is quick; what follows is Az building a context per subscription it can see, 25 of them by default, each a round trip. Use -MaxContextPopulation to cut it, and -Verbose to see where the time actually went. A SOVEREIGN CLOUD NEEDS -Environment AND FAILS OPAQUELY WITHOUT IT. Azure requires a separate sign-in per cloud; an account in one cannot see a tenant in another. Sent to the wrong authority the sign-in is refused before any prompt, with no mention of the cloud. .PARAMETER Subscription Subscription to select once signed in, by name or id. Without it you get whatever Azure considers your default, which on an account with many subscriptions is rarely the one you meant. .PARAMETER TenantId Tenant to sign in to. Needed when your account exists in more than one. .PARAMETER Environment The Azure cloud: AzureCloud (default), AzureChinaCloud, AzureUSGovernment. Required for a sovereign tenant - see the description. Tab-completes from Get-AzEnvironment, so a cloud registered locally with Add-AzEnvironment shows up too. Deliberately NOT a ValidateSet, which would refuse an air-gapped or national cloud that genuinely exists. .PARAMETER Scope Where the sign-in lives. 'Process' (default) keeps it in this shell. 'CurrentUser' writes it to the shared Az profile, making it the default for every new shell, which is Connect-AzAccount's own default. .PARAMETER MaxContextPopulation How many subscriptions Az builds a context for after sign-in. Az's own default is 25, and each one costs a round trip - which is why a sovereign sign-in can sit for a long time after you have already picked the account in the browser. Measured: ~0.08 s per ARM round trip on AzureCloud against ~2.3 s on AzureChinaCloud. Lower it when you only want one subscription; -1 populates all. It is NOT lowered by default, because those populated contexts are what lets Set-MsecContext move between clouds without a fresh sign-in. .PARAMETER UseDeviceCode Sign in with a device code instead of a browser. The way past the macOS broker failure, and the way in from a session with no browser at all. .PARAMETER PassThru Emit the full Az context as well as the summary. .EXAMPLE Connect-MsecAzure -Subscription 'PROD' Signs in for this shell only. Open another terminal and it is still pointed wherever it was - which is the point. .EXAMPLE Connect-MsecAzure -Environment AzureChinaCloud -UseDeviceCode A sovereign tenant from a Mac, avoiding both traps at once. .OUTPUTS One PSCustomObject describing the session, PSTypeName 'MsecAzureConnection'. .NOTES Signs in as YOU, not as the msec application - this is ARM, and the app holds Graph permissions, not Azure RBAC. What you can read is what your own role assignments allow. #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [string] $Subscription, [string] $TenantId, # COMPLETED, NOT VALIDATED. Get-AzEnvironment lists the clouds Az knows, which is three # out of the box - but Add-AzEnvironment registers more, and air-gapped and national # clouds are exactly the case where somebody needs this command most. A ValidateSet # would complete nicely and then refuse a cloud that genuinely exists. [ArgumentCompleter({ param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) $word = ([string]$wordToComplete).Trim("'`"") try { $envs = @(Get-AzEnvironment -ErrorAction Stop) } catch { return } $envs | Where-Object { $_.Name -like "$word*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new( $_.Name, $_.Name, 'ParameterValue', [string] $_.ResourceManagerUrl) } })] [string] $Environment, # SHELL-LOCAL BY DEFAULT, deliberately unlike Connect-AzAccount. See the help: the # credential is shared through the OS credential store whatever this says, so the only # thing being scoped is which tenant and subscription this shell points at. [ValidateSet('Process', 'CurrentUser')] [string] $Scope = 'Process', # THE LEVER FOR A SLOW CLOUD. After the browser step Az builds a context per # subscription it can see, capped at 25 by default, and each one is a round trip. # Measured: ~0.08 s per ARM round trip on AzureCloud against ~2.3 s on # AzureChinaCloud - 28x - so the populate phase that is invisible on the commercial # cloud is where a sovereign sign-in appears to hang after you have already picked # the account. Lower it when you only care about one subscription; -1 means all. # # NOT LOWERED BY DEFAULT, because those populated contexts are what lets # Set-MsecContext switch between clouds without signing in again. [int] $MaxContextPopulation, [switch] $UseDeviceCode, [switch] $PassThru ) if (-not (Get-Module -ListAvailable -Name Az.Accounts)) { throw 'Az.Accounts is required for Connect-MsecAzure. Install with: Install-Module Az.Accounts -Scope CurrentUser' } Import-Module Az.Accounts -ErrorAction Stop $connectArgs = @{ Scope = $Scope; ErrorAction = 'Stop' } if ($Subscription) { $connectArgs['Subscription'] = $Subscription } if ($TenantId) { $connectArgs['Tenant'] = $TenantId } if ($Environment) { $connectArgs['Environment'] = $Environment } if ($UseDeviceCode) { $connectArgs['UseDeviceAuthentication'] = $true } if ($PSBoundParameters.ContainsKey('MaxContextPopulation')) { $connectArgs['MaxContextPopulation'] = $MaxContextPopulation } Write-Verbose ("Connecting to {0} (scope {1}){2}" -f $(if ($Environment) { $Environment } else { 'AzureCloud' }), $Scope, $(if ($TenantId) { " in tenant $TenantId" } else { '' })) # TIMED, so "it hung" can be told from "the browser took a while". The sign-in itself is # quick; what follows it is not, on a cloud where every round trip costs seconds. $sw = [System.Diagnostics.Stopwatch]::StartNew() try { $null = Connect-AzAccount @connectArgs $sw.Stop() Write-Verbose ("Connect-AzAccount returned in {0:N1} s." -f $sw.Elapsed.TotalSeconds) } catch { $sw.Stop() # THE macOS BROKER FAILURE NAMED, because its own message does not name the cause and # the setting behind it is not something anyone would think to look for. $m = [string] $_.Exception.Message if ($m -match 'main thread' -or $m -match 'broker') { throw ("Azure sign-in failed because the macOS broker (WAM) cannot run on this thread. " + "Turn it off once with: Update-AzConfig -EnableLoginByWam `$false - or sign in " + "now with: Connect-MsecAzure -UseDeviceCode. Original error: $m") } throw "Azure sign-in failed: $m" } $ctx = Get-AzContext -ErrorAction SilentlyContinue if (-not $ctx) { throw 'Connect-AzAccount returned no context. The sign-in did not complete.' } # SAID OUT LOUD, because "which subscription am I actually on" is the question every one of # these commands depends on and the sign-in itself does not answer clearly. $out = [PSCustomObject]@{ PSTypeName = 'MsecAzureConnection' Account = [string] $ctx.Account.Id Subscription = [string] $ctx.Subscription.Name SubscriptionId = [string] $ctx.Subscription.Id TenantId = [string] $ctx.Tenant.Id Environment = [string] $ctx.Environment.Name Scope = $Scope } if ($Scope -eq 'CurrentUser') { Write-Verbose 'Scope CurrentUser: this is now the default context for every new shell.' } if ($PassThru) { $ctx } else { $out } } |