Private/SavedContext.ps1
|
# The saved sign-in (VM-6423): what signs a new PowerShell window in without a browser. # # A session lives in this module's scope only (Private/Session.ps1), and its refresh token cannot be shared: # it is single-use, so two windows holding one would revoke each other's session family the moment the second # one refreshed. What is saved instead is a saved sign-in, a credential the server exchanges for a new session # (POST /api/v1/cli/token, grant stored_grant) without spending it: every window that loads it gets a session # and a refresh chain of its own, and none signs another out. It acts as the signed-in user in the tenant it # was saved for, and ends with the sign-in it was saved from (30 days at most). An API token (Connect-VMetric # -ApiToken) is saved as it is: it is never refreshed, so windows can share it. # # The credential is kept in a Microsoft.PowerShell.SecretManagement vault, the default one, as one SecureString # secret named VirtualMetric.SavedContext that holds JSON. SecretManagement stays optional: the module never # imports it unless a sign-in is saved or loaded, and without it, or without a registered vault, nothing is # saved and the module behaves as before. # # Which vault holds it is recorded outside the vault, in a small file that holds no secret (the marker, # Get-VMetricSavedContextPath): a window looks into a vault only when the marker says a sign-in is saved # there, so a vault that asks for its password is never opened for a user who saved nothing. # # The first VirtualMetric cmdlet a new window runs loads it (Get-VMetricSession), once, unless the window has # signed in already. Saving another one in its place (Connect-VMetric -Persist, Save-VMetricContext) retires # the one it replaces on the server, leaving the windows that loaded it signed in; Disconnect-VMetric signs it # out on the server, which ends every session it minted, and removes it. $script:VMetricSavedSecretName = 'VirtualMetric.SavedContext' $script:VMetricSavedContextVersion = 1 # Pending until this window has loaded the saved sign-in or decided not to (Done). The tests set Disabled. $script:VMetricSavedContextState = 'Pending' # Where the marker is, when set: the tests point it into their own folder. $script:VMetricSavedContextPath = $null function Get-VMetricSavedContextPath { # The marker's path: $env:VMETRIC_SAVED_CONTEXT_PATH, else VirtualMetric/PowerShell/SavedContext.json in the # user's local application data. [CmdletBinding()] [OutputType([string])] param() if ($script:VMetricSavedContextPath) { return $script:VMetricSavedContextPath } if ($env:VMETRIC_SAVED_CONTEXT_PATH) { return $env:VMETRIC_SAVED_CONTEXT_PATH } $root = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::LocalApplicationData) if (-not $root) { $root = Join-Path -Path $HOME -ChildPath '.local/share' } Join-Path -Path $root -ChildPath 'VirtualMetric/PowerShell/SavedContext.json' } function Get-VMetricSavedContextMarker { # The marker: the vault that holds the saved sign-in, and the id it was saved under; $null when no # sign-in is saved, or the marker is not one this module wrote. [CmdletBinding()] [OutputType([pscustomobject])] param() $path = Get-VMetricSavedContextPath if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { return $null } try { $marker = ConvertFrom-VMetricJson -Json ([System.IO.File]::ReadAllText($path)) } catch { Write-Verbose "Ignoring $path, which is not the module's record of a saved sign-in: $($_.Exception.Message)" return $null } $vault = [string](Get-VMetricMember -InputObject $marker -Name 'vault') if ([long](Get-VMetricMember -InputObject $marker -Name 'version') -ne $script:VMetricSavedContextVersion -or -not $vault) { Write-Verbose "Ignoring $path, written by another version of the module." return $null } [pscustomobject]@{ Vault = $vault SavedId = [string](Get-VMetricMember -InputObject $marker -Name 'savedId') } } function Read-VMetricSavedContextMarker { # The vault the marker names; $null when no sign-in is saved. [CmdletBinding()] [OutputType([string])] param() $marker = Get-VMetricSavedContextMarker if ($marker) { return $marker.Vault } return $null } function Write-VMetricSavedContextMarker { # Records which vault holds the saved sign-in, and the random id it was saved under. Neither is a # secret. [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'The public cmdlets that save a sign-in ask ShouldProcess.')] [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Vault, [Parameter(Mandatory)] [string] $SavedId ) $path = Get-VMetricSavedContextPath $folder = Split-Path -Path $path -Parent if ($folder -and -not (Test-Path -LiteralPath $folder -PathType Container)) { $null = New-Item -ItemType Directory -Path $folder -Force -ErrorAction Stop } $text = ConvertTo-VMetricJson -InputObject ([ordered]@{ version = $script:VMetricSavedContextVersion; vault = $Vault; savedId = $SavedId }) [System.IO.File]::WriteAllText($path, $text, [System.Text.UTF8Encoding]::new($false)) } function Test-VMetricSessionPersisted { # Whether new windows sign in with a session's sign-in: the marker names the id the session saved, or # loaded, its sign-in under. Another window that signs the saved sign-in out, or saves another in its # place, changes the marker, so the answer is never a stale copy of what this window last did. [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory)] [System.Collections.IDictionary] $Session ) if (-not $Session.SavedId) { return $false } $marker = Get-VMetricSavedContextMarker return [bool]($marker -and $marker.SavedId -eq $Session.SavedId) } function Remove-VMetricSavedContextMarker { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'The public cmdlets that remove a sign-in ask ShouldProcess.')] [CmdletBinding()] param() $path = Get-VMetricSavedContextPath if (Test-Path -LiteralPath $path -PathType Leaf) { Remove-Item -LiteralPath $path -Force -ErrorAction Stop } } function Test-VMetricSecretManagement { # Whether Microsoft.PowerShell.SecretManagement can be used here: imported, or installed and importable. [CmdletBinding()] [OutputType([bool])] param() if (Get-Module -Name Microsoft.PowerShell.SecretManagement) { return $true } if (-not (Get-Module -ListAvailable -Name Microsoft.PowerShell.SecretManagement)) { return $false } try { Import-Module -Name Microsoft.PowerShell.SecretManagement -ErrorAction Stop -Verbose:$false return $true } catch { Write-Verbose "Microsoft.PowerShell.SecretManagement could not be imported: $($_.Exception.Message)" return $false } } # The calls into SecretManagement, one seam each, which the tests replace. function Get-VMetricSecretVaultList { [CmdletBinding()] param() Microsoft.PowerShell.SecretManagement\Get-SecretVault -ErrorAction Stop } function Read-VMetricSavedSecret { # The saved sign-in's text in a vault; $null when the vault holds none. [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [string] $Vault ) $info = @(Microsoft.PowerShell.SecretManagement\Get-SecretInfo -Name $script:VMetricSavedSecretName -Vault $Vault -ErrorAction Stop | Where-Object { $_.Name -eq $script:VMetricSavedSecretName }) if ($info.Count -eq 0) { return $null } [string](Microsoft.PowerShell.SecretManagement\Get-Secret -Name $script:VMetricSavedSecretName -Vault $Vault -AsPlainText -ErrorAction Stop) } function Write-VMetricSavedSecret { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'The public cmdlets that save a sign-in ask ShouldProcess.')] [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Vault, [Parameter(Mandatory)] [System.Security.SecureString] $Secret ) Microsoft.PowerShell.SecretManagement\Set-Secret -Name $script:VMetricSavedSecretName -Vault $Vault -SecureStringSecret $Secret ` -ErrorAction Stop -Confirm:$false -WhatIf:$false } function Remove-VMetricSavedSecret { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'The public cmdlets that remove a sign-in ask ShouldProcess.')] [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Vault ) Microsoft.PowerShell.SecretManagement\Remove-Secret -Name $script:VMetricSavedSecretName -Vault $Vault -ErrorAction Stop -Confirm:$false -WhatIf:$false } function Get-VMetricSaveVault { # The vault a sign-in is saved to: SecretManagement's default vault, or the only one registered. An error # that says what to install or register when there is none. [CmdletBinding()] [OutputType([string])] param() $setup = 'Install-Module Microsoft.PowerShell.SecretManagement, Microsoft.PowerShell.SecretStore; Register-SecretVault -Name SecretStore -ModuleName Microsoft.PowerShell.SecretStore -DefaultVault' if (-not (Test-VMetricSecretManagement)) { throw (New-VMetricException -Message "Keeping the sign-in for new windows needs Microsoft.PowerShell.SecretManagement and a vault, and the module is not installed. To set one up: $setup" ` -Code 'SecretManagementUnavailable' -Category NotInstalled -RecommendedAction $setup) } $vaults = @(Get-VMetricSecretVaultList) $default = @($vaults | Where-Object { $_.IsDefault }) if ($default.Count -gt 0) { return [string]$default[0].Name } if ($vaults.Count -eq 1) { return [string]$vaults[0].Name } if ($vaults.Count -eq 0) { throw (New-VMetricException -Message "Keeping the sign-in for new windows needs a SecretManagement vault, and none is registered. To register one: $setup" ` -Code 'SecretVaultUnavailable' -Category NotInstalled -RecommendedAction $setup) } throw (New-VMetricException -Message 'Several SecretManagement vaults are registered and none is the default: name one with Set-SecretVaultDefault.' ` -Code 'SecretVaultUnavailable' -Category NotSpecified -RecommendedAction 'Set-SecretVaultDefault -Name <vault>') } function ConvertFrom-VMetricSavedContext { # The saved sign-in's JSON as an object with its secret as a SecureString, its addresses validated; an error # for text this module did not write or cannot use. [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] [string] $Text, [Parameter(Mandatory)] [string] $Vault ) $data = ConvertFrom-VMetricJson -Json $Text $version = [long](Get-VMetricMember -InputObject $data -Name 'version') if ($version -ne $script:VMetricSavedContextVersion) { throw (New-VMetricException -Message "The saved sign-in in vault '$(ConvertTo-VMetricSafeText -Text $Vault)' was written by another version of the module (format $version)." ` -Code 'SavedContextUnsupported' -Category InvalidData) } $method = [string](Get-VMetricMember -InputObject $data -Name 'authMethod') $secret = [string](Get-VMetricMember -InputObject $data -Name 'secret') if ($method -notin @('SavedSignIn', 'ApiToken') -or -not $secret) { throw (New-VMetricException -Message "The saved sign-in in vault '$(ConvertTo-VMetricSafeText -Text $Vault)' is not one this module can use." ` -Code 'SavedContextInvalid' -Category InvalidData) } # Validated as an address given to Connect-VMetric is: https, plain http to this machine only. $apiUrl = ConvertTo-VMetricApiBase -Url ([string](ConvertTo-VMetricUrl -Uri ([string](Get-VMetricMember -InputObject $data -Name 'apiUrl')) -What 'the saved API address' -Strict)) $consoleUrl = ([string](ConvertTo-VMetricUrl -Uri ([string](Get-VMetricMember -InputObject $data -Name 'consoleUrl')) -What 'the saved console address' -Strict -Origin)).TrimEnd('/') [pscustomobject]@{ Vault = $Vault AuthMethod = $method ApiUrl = $apiUrl ConsoleUrl = $consoleUrl SkipCertificateCheck = [bool](Get-VMetricMember -InputObject $data -Name 'skipCertificateCheck') Secret = ConvertTo-VMetricSecureString -String $secret GrantId = [string](Get-VMetricMember -InputObject $data -Name 'grantId') SavedId = [string](Get-VMetricMember -InputObject $data -Name 'savedId') ExpiresAt = [long](Get-VMetricMember -InputObject $data -Name 'expiresAt') TenantName = [string](Get-VMetricMember -InputObject $data -Name 'tenantName') Email = [string](Get-VMetricMember -InputObject $data -Name 'email') } } function Get-VMetricSavedContext { # The saved sign-in the marker names: its vault, addresses and secret; $null when none is saved. An error # when the vault cannot be read or holds what this module cannot use. [CmdletBinding()] [OutputType([pscustomobject])] param() $vault = Read-VMetricSavedContextMarker if (-not $vault) { return $null } if (-not (Test-VMetricSecretManagement)) { throw (New-VMetricException -Message "A sign-in is saved in vault '$(ConvertTo-VMetricSafeText -Text $vault)', but Microsoft.PowerShell.SecretManagement is not installed to read it." ` -Code 'SecretManagementUnavailable' -Category NotInstalled) } try { $text = Read-VMetricSavedSecret -Vault $vault } catch { if (Test-VMetricFlowControl -ErrorRecord $_) { throw } throw (New-VMetricException -Message "Could not read the saved sign-in from vault '$(ConvertTo-VMetricSafeText -Text $vault)': $(ConvertTo-VMetricSafeText -Text $_.Exception.Message)" ` -Code 'SavedContextUnreadable' -Category ReadError -InnerException $_.Exception) } if (-not $text) { # Removed from the vault by hand: the marker is all that is left. Write-Verbose "Vault '$(ConvertTo-VMetricSafeText -Text $vault)' holds no saved sign-in any more." Remove-VMetricSavedContextMarker return $null } ConvertFrom-VMetricSavedContext -Text $text -Vault $vault } function Test-VMetricSameSecret { # Whether two SecureStrings hold the same text, compared in constant time. [CmdletBinding()] [OutputType([bool])] param( [AllowNull()] [System.Security.SecureString] $Left, [AllowNull()] [System.Security.SecureString] $Right ) if (-not $Left -or -not $Right) { return $false } $leftBytes = [System.Text.Encoding]::UTF8.GetBytes((ConvertFrom-VMetricSecureString -SecureString $Left)) $rightBytes = [System.Text.Encoding]::UTF8.GetBytes((ConvertFrom-VMetricSecureString -SecureString $Right)) return [System.Security.Cryptography.CryptographicOperations]::FixedTimeEquals($leftBytes, $rightBytes) } function Remove-VMetricSavedContextCopy { # Removes a saved sign-in from its vault, and the marker, unless the vault holds another one by now (a newer # save in another window): what is removed is only ever the one this window read. [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'The public cmdlets that remove a sign-in ask ShouldProcess; the loader removes only a saved sign-in the server refused.')] [CmdletBinding()] param( [Parameter(Mandatory)] [pscustomobject] $Saved ) $current = $null try { $current = Get-VMetricSavedContext } catch { if (Test-VMetricFlowControl -ErrorRecord $_) { throw } Write-Verbose "Reading the saved sign-in again: $($_.Exception.Message)" } if ($current -and -not (Test-VMetricSameSecret -Left $current.Secret -Right $Saved.Secret)) { Write-Verbose 'The vault holds a newer saved sign-in; it is kept.' return } Remove-VMetricSavedSecret -Vault $Saved.Vault Remove-VMetricSavedContextMarker } function Invoke-VMetricSavedGrantRevoke { # POST /cli/grants/revoke for a saved sign-in: signed out (its sessions end with it), or with -Replaced only # retired (a newer one took its place; the sessions it minted run on). [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'The public cmdlets that call it ask ShouldProcess.')] [CmdletBinding()] param( [Parameter(Mandatory)] [pscustomobject] $Saved, [switch] $Replaced ) $body = [ordered]@{ grant = $Saved.Secret } if ($Replaced) { $body['replaced'] = $true } $null = Invoke-VMetricRequest -Method POST -Path '/cli/grants/revoke' -Anonymous -ApiUrl $Saved.ApiUrl ` -SkipCertificateCheck:([bool]$Saved.SkipCertificateCheck) -NoConflictRetry -Body $body } function Save-VMetricSavedContext { # Saves a session's sign-in for new windows: a saved sign-in minted for it (POST /cli/grants), or its API # token, in the default vault, and the marker. The one it replaces is retired on the server and removed. # Answers the vault's name. [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'The public cmdlets that call it ask ShouldProcess.')] [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [System.Collections.IDictionary] $Session ) $vault = Get-VMetricSaveVault $previous = $null try { $previous = Get-VMetricSavedContext } catch { if (Test-VMetricFlowControl -ErrorRecord $_) { throw } Write-Verbose "The sign-in saved before could not be read, so it is not retired on the server: $($_.Exception.Message)" } # What tells this save from any other, in the vault and in the marker: Test-VMetricSessionPersisted # compares it with the session's. $savedId = [guid]::NewGuid().ToString('N') $data = [ordered]@{ version = $script:VMetricSavedContextVersion savedId = $savedId authMethod = 'SavedSignIn' apiUrl = [string]$Session.ApiUrl consoleUrl = [string]$Session.ConsoleUrl skipCertificateCheck = [bool]$Session.SkipCertificateCheck secret = $null grantId = '' expiresAt = 0 tenantId = [string]$Session.TenantId tenantName = [string]$Session.TenantName email = [string]$Session.Email } $minted = $null if ($Session.AuthMethod -eq 'ApiToken') { $data.authMethod = 'ApiToken' $data.secret = $Session.AccessToken } else { try { $answer = Invoke-VMetricRequest -Method POST -Path '/cli/grants' -Session $Session -NoConflictRetry } catch { if (Test-VMetricFlowControl -ErrorRecord $_) { throw } $inner = $_.Exception if ($inner -is [VMetricException] -and $inner.Code -eq 'CLI_GRANT_NOT_ALLOWED') { throw (New-VMetricException -Message 'This session started from the saved sign-in, which a saved sign-in cannot save again. To keep another tenant or account for new windows, sign in with Connect-VMetric -Persist.' ` -Code 'CLI_GRANT_NOT_ALLOWED' -Category PermissionDenied -StatusCode $inner.StatusCode -TraceId $inner.TraceId -InnerException $inner) } throw } $grant = [string](Get-VMetricMember -InputObject $answer -Name 'grant') if (-not $grant) { throw (New-VMetricException -Message 'The server answered no saved sign-in.' -Code 'InvalidGrantResponse' -Category InvalidResult) } $data.secret = ConvertTo-VMetricSecureString -String $grant $grant = $null $data.grantId = [string](Get-VMetricMember -InputObject $answer -Name 'id') $data.expiresAt = [long](Get-VMetricMember -InputObject $answer -Name 'expiresAt') $minted = [pscustomobject]@{ ApiUrl = $Session.ApiUrl; SkipCertificateCheck = [bool]$Session.SkipCertificateCheck; Secret = $data.secret } } try { $text = ConvertTo-VMetricJson -InputObject $data $secure = ConvertTo-VMetricSecureString -String $text $text = $null Write-VMetricSavedSecret -Vault $vault -Secret $secure $secure.Dispose() Write-VMetricSavedContextMarker -Vault $vault -SavedId $savedId } catch { if (Test-VMetricFlowControl -ErrorRecord $_) { throw } $inner = $_.Exception # Nothing holds the saved sign-in just minted: sign it out rather than leave it live. if ($minted) { try { Invoke-VMetricSavedGrantRevoke -Saved $minted } catch { Write-Verbose "Signing out the saved sign-in that could not be kept: $($_.Exception.Message)" } } throw (New-VMetricException -Message "Could not keep the sign-in in vault '$(ConvertTo-VMetricSafeText -Text $vault)': $(ConvertTo-VMetricSafeText -Text $inner.Message)" ` -Code 'SavedContextNotWritten' -Category WriteError -InnerException $inner) } if ($previous) { if ($previous.Vault -ne $vault) { try { Remove-VMetricSavedSecret -Vault $previous.Vault } catch { Write-Warning "The sign-in saved before in vault '$(ConvertTo-VMetricSafeText -Text $previous.Vault)' could not be removed: $(ConvertTo-VMetricSafeText -Text $_.Exception.Message)" } } if ($previous.AuthMethod -eq 'SavedSignIn' -and -not (Test-VMetricSameSecret -Left $previous.Secret -Right $data.secret)) { try { Invoke-VMetricSavedGrantRevoke -Saved $previous -Replaced } catch { if (Test-VMetricFlowControl -ErrorRecord $_) { throw } Write-Warning "The sign-in saved before could not be retired on the server ($($_.Exception.Message)); it ends on its own, or revoke it in the console's list of PowerShell sessions." } } } $Session.SavedId = $savedId $vault } function Import-VMetricSavedContext { # The loader: in a window that has not signed in, the first call signs in with the saved sign-in and answers # the session; $null when nothing is saved or it cannot be used, said in a warning. A saved sign-in the # server refuses is removed; one that fails for another reason is kept, and the next call tries again. [CmdletBinding()] [OutputType([hashtable])] param() if ($script:VMetricSavedContextState -ne 'Pending') { return $null } # A window that signed in, or holds a session, is not signed in again behind its back. if ($script:VMetricSession -or $script:VMetricSessions.Count -gt 0) { $script:VMetricSavedContextState = 'Done' return $null } $attempt = 0 while ($true) { $attempt++ try { $saved = Get-VMetricSavedContext } catch { if (Test-VMetricFlowControl -ErrorRecord $_) { throw } $script:VMetricSavedContextState = 'Done' Write-Warning "The saved sign-in could not be used: $($_.Exception.Message)" return $null } if (-not $saved) { $script:VMetricSavedContextState = 'Done' return $null } if ($saved.AuthMethod -eq 'SavedSignIn' -and $saved.ExpiresAt -gt 0 -and [System.DateTimeOffset]::FromUnixTimeSeconds($saved.ExpiresAt) -le [System.DateTimeOffset]::UtcNow) { Write-Verbose 'The saved sign-in has ended; it is removed.' Remove-VMetricSavedContextCopy -Saved $saved $script:VMetricSavedContextState = 'Done' return $null } if ($saved.SkipCertificateCheck) { Write-Warning 'Certificate checks are off for the saved sign-in: the server''s identity is not verified, so anyone between you and it could read your tokens. Use this only with a test server.' } $address = @{ ApiUrl = $saved.ApiUrl; ConsoleUrl = $saved.ConsoleUrl; SkipCertificateCheck = $saved.SkipCertificateCheck } try { if ($saved.AuthMethod -eq 'ApiToken') { $session = New-VMetricSession @address -AuthMethod ApiToken -ApiToken $saved.Secret Initialize-VMetricApiTokenSession -Session $session } else { $answer = Invoke-VMetricTokenRequest -ApiUrl $saved.ApiUrl -SkipCertificateCheck:([bool]$saved.SkipCertificateCheck) -Body ([ordered]@{ grantType = 'stored_grant' grant = $saved.Secret }) $session = New-VMetricSession @address -AuthMethod SavedSignIn -TokenResponse $answer } } catch { if (Test-VMetricFlowControl -ErrorRecord $_) { throw } $failure = $_.Exception $code = '' $status = 0 if ($failure -is [VMetricException]) { $code = $failure.Code $status = $failure.StatusCode } if ($code -in @('invalid_grant', 'access_denied', 'ApiTokenRefused')) { # Revoked, ended, or no longer the user's to use. A newer one saved meanwhile by another window # is tried once; this one is removed. $current = $null try { $current = Get-VMetricSavedContext } catch { Write-Verbose "Reading the saved sign-in again: $($_.Exception.Message)" } if ($attempt -eq 1 -and $current -and -not (Test-VMetricSameSecret -Left $current.Secret -Right $saved.Secret)) { Write-Verbose 'Another window saved a newer sign-in meanwhile; using it.' continue } try { Remove-VMetricSavedContextCopy -Saved $saved } catch { Write-Verbose "Removing the saved sign-in: $($_.Exception.Message)" } $script:VMetricSavedContextState = 'Done' Write-Warning "The saved sign-in can no longer be used ($($failure.Message)); it was removed. Run Connect-VMetric -Persist to sign in and keep the sign-in again." return $null } if ($code -eq 'ConnectionFailed' -or $status -ge 500 -or $status -eq 429) { Write-Warning "Could not sign in with the saved sign-in ($($failure.Message)); the next command tries again." return $null } $script:VMetricSavedContextState = 'Done' Write-Warning "Could not sign in with the saved sign-in: $($failure.Message)" return $null } $session.SavedId = $saved.SavedId $script:VMetricSavedContextState = 'Done' Register-VMetricSession -Session $session $account = if ($session.AuthMethod -eq 'ApiToken') { 'an API token' } else { ConvertTo-VMetricSafeText -Text ([string]$session.Email) } $tenantText = if ($session.TenantName) { " in tenant '$(ConvertTo-VMetricSafeText -Text ([string]$session.TenantName))'" } else { '' } Write-Verbose "Signed in to $($session.ConsoleUrl) as $account$tenantText with the sign-in saved in vault '$(ConvertTo-VMetricSafeText -Text $saved.Vault)'." return $session } } function Remove-VMetricSavedContext { # Disconnect-VMetric's part: signs the saved sign-in out on the server, which ends every session it minted # (an API token stays valid), and removes it from its vault and the marker. A sign-out the server does not # take is a warning: the local copy goes anyway. Answers whether a sign-in was saved. [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Disconnect-VMetric asks ShouldProcess.')] [CmdletBinding()] [OutputType([bool])] param() $vault = Read-VMetricSavedContextMarker if (-not $vault) { return $false } $saved = $null $unreadable = $false try { # $null when the vault holds none any more: the marker was all that was left, and is gone. $saved = Get-VMetricSavedContext } catch { if (Test-VMetricFlowControl -ErrorRecord $_) { throw } $unreadable = $true Write-Warning "The saved sign-in could not be read to sign it out on the server ($($_.Exception.Message)): revoke it in the console's list of PowerShell sessions." } if ($saved -and $saved.AuthMethod -eq 'SavedSignIn') { try { Invoke-VMetricSavedGrantRevoke -Saved $saved } catch { if (Test-VMetricFlowControl -ErrorRecord $_) { throw } Write-Warning "Could not sign out the saved sign-in on the server ($($_.Exception.Message)); it ends on its own, or revoke it in the console's list of PowerShell sessions." } } elseif ($saved) { Write-Verbose 'The saved API token is removed from the vault; revoke the token itself in the console.' } try { # What was read is removed, and what could not be read is tried; a vault that holds nothing # any more is left alone. if ($saved -or ($unreadable -and (Test-VMetricSecretManagement))) { Remove-VMetricSavedSecret -Vault $vault } } catch { if (Test-VMetricFlowControl -ErrorRecord $_) { throw } Write-Warning "The saved sign-in could not be removed from vault '$(ConvertTo-VMetricSafeText -Text $vault)': $(ConvertTo-VMetricSafeText -Text $_.Exception.Message)" } Remove-VMetricSavedContextMarker return $true } |