Providers/PingOne/Public/Connect-PingOneEnvironment.ps1
|
function Connect-PingOneEnvironment { <# .SYNOPSIS Establishes the PingOne connection every other function in this provider uses .DESCRIPTION PingOne authenticates a worker application with a client id and secret, and that is the only credential this provider needs. The worker application is created by hand in the console, with its roles, before the first connect; this provider does not create it. Two things about that are easy to get wrong, and both are handled here rather than left to the caller. The token endpoint belongs to the environment the worker application LIVES in, not the one it manages. A trial hands you an Administrators environment and a sandbox, and the obvious thing to do - create the worker where the objects will go - is not what most people end up doing, because the console's application list opens on Administrators. Authenticating against the wrong one is refused with `invalid_client`, which is the same message you get for a disabled application and for a mistyped secret, so it sends people looking in the wrong place. -AuthEnvironmentId says where the application lives and defaults to -EnvironmentId, which is right when they are the same and harmless to state when they are not. The region is not derivable from an environment id. PingOne serves North America, Europe, Canada, Asia-Pacific and Australia from different hostnames, and asking the wrong one returns a 404 that looks like a missing environment. The console's own hostname is the reliable answer, so -Region is named rather than guessed, and the error below says which host was tried. The connection is validated before it is stored. A credential accepted here and refused on the first real call gives somebody an error about users when the problem is the credential, so the environment is read once up front - which also collects the name and licence the report and the seed both want. Prefix and EmailDomain are recorded on the connection rather than passed to every function. They are what teardown keys off, so setting them once removes the failure mode where an environment is seeded under one prefix and torn down under another. .PARAMETER EnvironmentId The environment to seed. A GUID, shown on the environment's home page in the console. .PARAMETER ClientId The worker application's client id. Also a GUID, and on a worker application it is the same value as the application's own id. .PARAMETER ClientSecret The worker application's secret, as a SecureString. .PARAMETER UseStoredSecret Read the secret from this machine's record for the environment instead of being given it. Written by -SaveSecret on an earlier connect. Also answers to -UseStoredCredential, the name every provider shares for connecting with what it stored. .PARAMETER SaveSecret Write the secret to this machine's record once the connection has been proved, so later runs can use -UseStoredSecret. Nothing is written if the connection fails. .PARAMETER AuthEnvironmentId The environment the worker application lives in, when that is not the one being seeded. Defaults to -EnvironmentId. .PARAMETER Region The region the tenant is served from. Defaults to NorthAmerica, which is where a trial lands. .PARAMETER Prefix Name prefix and seed tag for everything this module creates. Everything created and everything removed is scoped by it. .PARAMETER EmailDomain Domain for seeded usernames and emails. The default is under example.com, which RFC 2606 reserves precisely so test data cannot deliver mail to a real recipient. Change it only if you own the domain you change it to. .PARAMETER PassThru Return the connection object. .OUTPUTS PSCustomObject describing the connection, when -PassThru is used. .EXAMPLE PS> $secret = Read-Host 'Worker secret' -AsSecureString PS> Connect-PingOneEnvironment -EnvironmentId 0e2469df-9492-43bf-b8d6-8b9023795e55 ` -ClientId bb016fd3-0157-40ac-8ca3-74eda168d0d9 -ClientSecret $secret ` -AuthEnvironmentId ed8e5fd7-15dc-4ac4-8665-6adb1b88ed51 -SaveSecret DESCRIPTION: First run, with the worker application living in the Administrators environment OUTPUT: Nothing, unless -PassThru is given USE CASE: Connecting to a trial, where the worker is usually not in the sandbox .EXAMPLE PS> Connect-PingOneEnvironment -EnvironmentId 0e2469df-... -ClientId bb016fd3-... ` -UseStoredSecret -AuthEnvironmentId ed8e5fd7-... DESCRIPTION: Every run after that OUTPUT: Nothing USE CASE: The ordinary case, with nothing to paste .NOTES Author: Jeffrey Stuhr Blog: https://www.techbyjeff.net LinkedIn: https://www.linkedin.com/in/jeffrey-stuhr-034214aa/ .LINK New-PingOneEnvironment Get-PingOneEnvironmentReport Disconnect-PingOneEnvironment #> [CmdletBinding(DefaultParameterSetName = 'Secret')] [OutputType([PSCustomObject])] param( [Parameter(Mandatory = $true)] [ValidatePattern('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')] [string]$EnvironmentId, [Parameter(Mandatory = $true)] [ValidatePattern('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')] [string]$ClientId, [Parameter(Mandatory = $true, ParameterSetName = 'Secret')] [System.Security.SecureString]$ClientSecret, [Parameter(Mandatory = $true, ParameterSetName = 'Stored')] [Alias('UseStoredCredential')] [switch]$UseStoredSecret, [Parameter(ParameterSetName = 'Secret')] [switch]$SaveSecret, [Parameter()] [ValidatePattern('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')] [string]$AuthEnvironmentId, [Parameter()] [ValidateSet('NorthAmerica', 'Europe', 'Canada', 'AsiaPacific', 'Australia')] [string]$Region = 'NorthAmerica', [Parameter()] [ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_-]{0,30}$|^[A-Za-z0-9][A-Za-z0-9_-]*[-_]$')] [string]$Prefix = $script:TestEnvironmentDefaultPrefix, [Parameter()] [ValidateNotNullOrEmpty()] [string]$EmailDomain = $script:PingOneDefaultSeedDomain, [Parameter()] [switch]$PassThru ) $correlationId = [Guid]::NewGuid() Write-Verbose "Starting Connect-PingOneEnvironment - CorrelationId: $correlationId" if (-not $AuthEnvironmentId) { $AuthEnvironmentId = $EnvironmentId } $secret = $ClientSecret if ($UseStoredSecret) { $recordPath = Get-PingOneCredentialPath -EnvironmentId $EnvironmentId if (-not (Test-Path -LiteralPath $recordPath)) { throw ("No stored secret for environment $EnvironmentId at $recordPath. Connect once " + 'with -ClientSecret and -SaveSecret first.') } $secret = (Get-Content -LiteralPath $recordPath -Raw).Trim() | ConvertTo-SecureString } $hosts = $script:PingOneRegionHost[$Region] # Held in a local until it is proved, so a failed connect cannot leave a half-built # connection in module scope for the next call to trip over. $candidate = @{ EnvironmentId = $EnvironmentId AuthEnvironmentId = $AuthEnvironmentId ClientId = $ClientId ClientSecret = $secret Region = $Region ApiHost = $hosts.Api AuthHost = $hosts.Auth ConsoleHost = $hosts.Console Prefix = $Prefix EmailDomain = $EmailDomain AccessToken = $null TokenExpiresUtc = $null TokenScopes = @() } # The cheapest possible call that proves all three of the credential, the region and the # environment id at once. # Named in the absolute form below /v1, because the environment itself is the one thing # that is not a collection underneath the environment. try { $environment = Invoke-PingOneRequest -Method GET -Path "/environments/$EnvironmentId" ` -Connection $candidate -ErrorAction Stop } catch { throw ("Could not read environment $EnvironmentId from $($hosts.Api): $($_.Exception.Message) " + "If the environment exists, check -Region - it defaults to NorthAmerica and the region " + 'is not derivable from the id.') } $candidate['EnvironmentName'] = $environment.name $candidate['EnvironmentType'] = $environment.type $candidate['OrganizationId'] = $environment.organization.id $candidate['LicenseId'] = $environment.license.id $script:PingOneConnection = $candidate if ($SaveSecret) { $recordPath = Get-PingOneCredentialPath -EnvironmentId $EnvironmentId ConvertFrom-SecureString -SecureString $secret | Set-Content -LiteralPath $recordPath -Encoding utf8 Write-Verbose "Wrote the worker secret to $recordPath" } Write-Verbose ("Connected to PingOne environment '{0}' ({1}) in {2}" -f $environment.name, $EnvironmentId, $Region) if ($PassThru) { return [PSCustomObject]@{ PSTypeName = 'PingOneConnection' EnvironmentId = $EnvironmentId EnvironmentName = $environment.name EnvironmentType = $environment.type AuthEnvironmentId = $AuthEnvironmentId OrganizationId = $environment.organization.id ClientId = $ClientId Region = $Region ApiHost = $hosts.Api Prefix = $Prefix EmailDomain = $EmailDomain } } } |