Public/Connect-Snowplow.ps1
|
function Connect-Snowplow { <# .SYNOPSIS Connects to a ServiceNow instance and stores the session in module scope. .DESCRIPTION Authorization code with PKCE is the default interactive grant and opens a browser. Use -ClientCredentials for unattended automation with a dedicated service account. Basic authentication is retained for compatibility and emits a deprecation warning. Sessions require HTTPS. Prod-tagged profiles warn on connect and are read-only unless -AllowWrite is supplied. .PARAMETER Instance The instance name (dev12345) or a full https URL. .PARAMETER Profile The name of a saved profile created by Add-SnowplowProfile. .PARAMETER AuthMethod OAuth (default) or Basic. .PARAMETER ClientId The OAuth application client ID for the authorization code grant. .PARAMETER AuthorizationCode Explicitly selects the authorization code with PKCE grant. .PARAMETER ClientCredentials Selects the client credentials grant for unattended use. .PARAMETER RedirectPort The local loopback port (1024-65535) used to receive the authorization code. .PARAMETER NoBrowser Prints the sign-in URL instead of opening a browser, for SSH sessions and containers. .PARAMETER ClientCredential A PSCredential whose username is the OAuth client ID and password is the client secret. .PARAMETER Credential A PSCredential for Basic authentication. .PARAMETER AccessToken An existing bearer token to use as-is. .PARAMETER ClientCredentialSecretName The name of a SecretManagement secret holding the client credential. .PARAMETER CredentialSecretName The name of a SecretManagement secret holding the Basic credential. .PARAMETER Vault The SecretManagement vault to read secrets from. .PARAMETER AllowWrite Allows writes on a Prod-tagged profile. .PARAMETER SkipValidation Skips the connectivity check made after authenticating. .EXAMPLE Connect-Snowplow -Instance dev12345 -ClientId 'your-oauth-client-id' Interactive sign-in. Your browser opens, you sign in (including SSO and MFA), and the module keeps the token. .EXAMPLE Connect-Snowplow -Instance dev12345 -ClientId 'your-oauth-client-id' -NoBrowser Sign in from a headless machine; open the printed URL on any device. .EXAMPLE Connect-Snowplow -Instance dev12345 -ClientCredential (Get-Credential) -ClientCredentials Unattended sign-in with a service account's client ID and secret. .EXAMPLE Connect-Snowplow -Instance dev12345 -ClientCredentialSecretName SnowplowOAuth -Vault SnowplowVault -ClientCredentials Reads the client credential from SecretManagement so no secret appears in your script. .EXAMPLE Connect-Snowplow -Profile prod -ClientCredentials -AllowWrite Connects to a Prod profile with writes explicitly enabled. #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Position = 0)][string]$Instance, [ArgumentCompleter({ param($commandName, $parameterName, $wordToComplete) & (Get-Module Snowplow) { param($word) Get-SnowCompletionValue -Kind Profile -Word $word } $wordToComplete })] [string]$Profile, [ValidateSet('OAuth', 'Basic')][string]$AuthMethod = 'OAuth', [string]$ClientId, [switch]$AuthorizationCode, [switch]$ClientCredentials, [ValidateRange(1024, 65535)][int]$RedirectPort = 8765, [switch]$NoBrowser, # OAuth client id (UserName) and client secret (Password). [pscredential]$ClientCredential, # Basic auth user, or the user for the legacy OAuth password grant. [pscredential]$Credential, # Existing OAuth access token for headless or CI use; it is kept in memory only. [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })][string]$AccessToken, # SecretManagement secret names (must resolve to a PSCredential). [string]$ClientCredentialSecretName, [string]$CredentialSecretName, [string]$Vault, [switch]$AllowWrite, [switch]$SkipValidation ) $storedProfile = $null if ($Profile) { $storedProfile = Get-SnowStoredProfile -Name $Profile if (-not $Instance) { $Instance = $storedProfile.Instance } if (-not $PSBoundParameters.ContainsKey('AuthMethod')) { $AuthMethod = $storedProfile.AuthMethod } if (-not $PSBoundParameters.ContainsKey('ClientCredentialSecretName') -and -not $PSBoundParameters.ContainsKey('ClientCredential') -and -not $AccessToken) { $ClientCredentialSecretName = $storedProfile.ClientCredentialSecretName } if (-not $PSBoundParameters.ContainsKey('CredentialSecretName') -and -not $PSBoundParameters.ContainsKey('Credential') -and -not $AccessToken) { $CredentialSecretName = $storedProfile.CredentialSecretName } if (-not $PSBoundParameters.ContainsKey('Vault')) { $Vault = $storedProfile.Vault } } if ($AuthorizationCode -and $ClientCredentials) { throw [System.ArgumentException]::new('Choose either -AuthorizationCode or -ClientCredentials, not both.') } if ($AuthMethod -eq 'Basic' -and ($AuthorizationCode -or $ClientId -or $ClientCredentials)) { throw [System.ArgumentException]::new('OAuth grant parameters cannot be combined with -AuthMethod Basic.') } if (($AuthorizationCode -or $ClientId) -and ($ClientCredential -or $ClientCredentialSecretName -or $Credential -or $CredentialSecretName -or $AccessToken)) { throw [System.ArgumentException]::new('Authorization code cannot be combined with client credentials, user credentials, or -AccessToken.') } $useAuthorizationCode = $AuthMethod -eq 'OAuth' -and ( $AuthorizationCode -or $ClientId -or (-not $ClientCredentials -and -not $ClientCredential -and -not $ClientCredentialSecretName -and -not $Credential -and -not $CredentialSecretName -and -not $AccessToken) ) if ($ClientCredentials -and (-not $ClientCredential -and -not $ClientCredentialSecretName -or $Credential -or $CredentialSecretName)) { throw [System.ArgumentException]::new('-ClientCredentials requires a client credential and cannot be combined with a user credential.') } if ($useAuthorizationCode -and -not $ClientId) { throw [System.ArgumentException]::new('Authorization code requires -ClientId. For unattended use, specify -ClientCredentials and a client credential.') } if ($AccessToken -and ($AuthMethod -eq 'Basic' -or $ClientCredential -or $ClientCredentialSecretName -or $Credential -or $CredentialSecretName)) { throw [System.ArgumentException]::new('-AccessToken cannot be combined with Basic authentication or credential parameters.') } if (-not $Instance) { throw [System.ArgumentException]::new('Specify -Instance or -Profile.') } $baseUri = Resolve-SnowInstanceUri -Instance $Instance $fromVault = { param($name) if (-not (Get-Command Get-Secret -ErrorAction SilentlyContinue)) { throw [System.InvalidOperationException]::new('Microsoft.PowerShell.SecretManagement is required to use secret names.') } $args2 = @{ Name = $name; ErrorAction = 'Stop' } if ($Vault) { $args2.Vault = $Vault } $secret = Get-Secret @args2 if ($secret -isnot [pscredential]) { throw [System.ArgumentException]::new("Secret '$name' must be a PSCredential.") } $secret } if ($ClientCredentialSecretName) { $ClientCredential = & $fromVault $ClientCredentialSecretName } if ($CredentialSecretName) { $Credential = & $fromVault $CredentialSecretName } if ($AuthMethod -eq 'Basic') { if (-not $Credential) { if ($Profile) { $Credential = Get-Credential -Message "Enter the basic authentication credential for Snowplow profile '$Profile'" } else { throw [System.ArgumentException]::new('Basic auth requires -Credential or -CredentialSecretName.') } } Write-Warning 'Basic authentication is deprecated for ServiceNow integrations. Prefer OAuth (-ClientCredential).' } elseif (-not $useAuthorizationCode) { if (-not $ClientCredential -and -not $AccessToken) { if ($Profile) { $ClientCredential = Get-Credential -Message "Enter the OAuth client credential for Snowplow profile '$Profile'" } else { throw [System.ArgumentException]::new('OAuth requires -ClientCredential or -ClientCredentialSecretName.') } } if ($Credential) { Write-Warning 'The OAuth password grant is legacy; prefer client credentials where your release supports it.' } } $environment = if ($storedProfile) { $storedProfile.Environment } else { $null } $readOnly = $environment -eq 'Prod' -and -not $AllowWrite if ($environment -eq 'Prod') { if ($readOnly) { Write-Warning "Connected to Prod profile '$Profile'. Snowplow is read-only by default; reconnect with -AllowWrite to enable writes." } else { Write-Warning "Connected to Prod profile '$Profile'. Write operations require confirmation." } } $session = [pscustomobject]@{ BaseUri = $baseUri AuthMethod = $AuthMethod Credential = $Credential ClientCredential = $ClientCredential ClientId = if ($ClientId) { $ClientId } elseif ($ClientCredential) { $ClientCredential.UserName } else { $null } GrantType = if ($useAuthorizationCode) { 'authorization_code' } elseif ($Credential) { 'password' } else { 'client_credentials' } RedirectPort = $RedirectPort NoBrowser = [bool]$NoBrowser RefreshToken = $null AccessToken = if ($AccessToken) { ConvertTo-SecureString -String $AccessToken -AsPlainText -Force } else { $null } ExpiresAt = if ($AccessToken) { [datetime]::MaxValue } else { [datetime]::MinValue } ConnectedAt = [datetime]::UtcNow Profile = $Profile Environment = $environment DefaultBackend = if ($storedProfile) { $storedProfile.DefaultBackend } else { $script:SnowplowConfig.DefaultBackend } AllowWrite = [bool]$AllowWrite ReadOnly = $readOnly } $previous = $script:SnowplowSession $previousDefaultBackend = $script:SnowplowConfig.DefaultBackend if ($storedProfile) { $script:SnowplowConfig.DefaultBackend = $storedProfile.DefaultBackend } $script:SnowplowSession = $session try { if ($useAuthorizationCode -and -not $AccessToken) { $session.RefreshToken = Read-SnowOAuthRefreshToken -BaseUri $baseUri -ClientId $session.ClientId $token = Request-SnowOAuthToken -Session $session $session.AccessToken = $token.AccessToken $session.ExpiresAt = $token.ExpiresAt } if (-not $SkipValidation) { # Fetches a token (OAuth) and proves the credentials can reach the Table API. $null = Invoke-SnowRequest -Table sys_user -Fields sys_id -First 1 -PageSize 1 -MaxRetries 0 -TimeoutSeconds 30 } } catch { $script:SnowplowSession = $previous $script:SnowplowConfig.DefaultBackend = $previousDefaultBackend throw } $script:SnowSchemaCache.Tables = $null $script:SnowSchemaCache.Fields = @{} [pscustomobject]@{ PSTypeName = 'Snowplow.Session' Instance = $baseUri.Host BaseUri = $baseUri AuthMethod = $AuthMethod Profile = $Profile Environment = $environment DefaultBackend = $script:SnowplowConfig.DefaultBackend ReadOnly = $readOnly } } |