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. Legacy password and Basic authentication are retained for compatibility. .EXAMPLE Connect-Snowplow -Instance dev12345 -ClientId '<oauth-client-id>' .EXAMPLE Connect-Snowplow -Instance dev12345 -ClientCredential (Get-Credential) -ClientCredentials .EXAMPLE Connect-Snowplow -Instance dev12345 -ClientCredentialSecretName SnowClient -Vault MyVault -ClientCredentials #> [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 } } 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 } } |