Providers/Authentik/Public/New-AuthentikApplication.ps1
|
function New-AuthentikApplication { <# .EXTERNALHELP TestEnvironment-Help.xml .SYNOPSIS Creates the seeded Authentik applications and the providers behind them #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] [OutputType([PSCustomObject])] param( [Parameter()] [string[]]$ApplicationName, [Parameter()] [switch]$SkipProvider, [Parameter()] [switch]$PassThru ) $connection = Get-AuthentikConnection $marker = Get-AuthentikSeedMarker -Connection $connection $csvPath = Join-Path -Path (Get-AuthentikDataPath) -ChildPath 'AuthentikApplications.csv' $rows = @(Import-Csv -Path $csvPath -Encoding UTF8) if ($ApplicationName) { $rows = @($rows | Where-Object { $ApplicationName -contains $_.Name }) $unknown = @($ApplicationName | Where-Object { $rows.Name -notcontains $_ }) if ($unknown) { throw "No definition in $csvPath for: $($unknown -join ', ')" } } $result = [PSCustomObject]@{ TotalApplications = $rows.Count CreatedApplications = 0 UpdatedApplications = 0 ProvidersCreated = 0 Applications = @() Errors = @() } # The seed URLs are written against a placeholder domain, substituted with the # connection's own so redirect URIs and launch URLs belong to whatever domain the users do. $substitute = { param($value) if ([string]::IsNullOrWhiteSpace($value)) { return $value } $value -replace [regex]::Escape($script:AuthentikDefaultSeedDomain), $connection.EmailDomain } $existingBySlug = @{} foreach ($existing in (Get-AuthentikSeededObject -Type Applications -Connection $connection)) { $existingBySlug[[string]$existing.slug] = $existing } $existingProviderByName = @{} foreach ($existing in (Get-AuthentikSeededObject -Type Providers -Connection $connection)) { $existingProviderByName[[string]$existing.name] = $existing } $applications = [System.Collections.Generic.List[object]]::new() foreach ($row in $rows) { $name = '{0}{1}' -f $marker.Prefix, $row.Name $slug = '{0}-{1}' -f $marker.SlugPrefix, $row.Slug if (-not $PSCmdlet.ShouldProcess($name, 'Create Authentik application')) { continue } try { $providerPk = $null $providerType = if ($SkipProvider) { 'None' } else { $row.ProviderType } if ($providerType -ne 'None') { $providerName = '{0} Provider' -f $name if ($existingProviderByName.ContainsKey($providerName)) { $providerPk = [int]$existingProviderByName[$providerName].pk Write-Verbose "Reusing provider $providerName" } else { $authorization = Get-AuthentikFlow -Designation authorization -Connection $connection $invalidation = Get-AuthentikFlow -Designation invalidation -Connection $connection # Provider-specific settings ride in one CSV cell. URLs in it are written # against the seed domain like every other URL, and a list joins with a # comma where the API wants one string. $settings = ConvertFrom-AuthentikSetting -Text $row.Settings foreach ($key in @($settings.Keys)) { if ($settings[$key] -is [string]) { $settings[$key] = & $substitute $settings[$key] } } # The API attaches no property mappings, where the admin UI would select the # instance's defaults. Without them an OAuth2 provider issues tokens with no # claims and a SAML provider assertions with no attributes, so a client # cannot sign in. Each provider gets the defaults the UI would give it. $openId = @('goauthentik.io/providers/oauth2/scope-openid', 'goauthentik.io/providers/oauth2/scope-email', 'goauthentik.io/providers/oauth2/scope-profile') $samlDefaults = @('goauthentik.io/providers/saml/upn', 'goauthentik.io/providers/saml/name', 'goauthentik.io/providers/saml/email', 'goauthentik.io/providers/saml/username', 'goauthentik.io/providers/saml/uid', 'goauthentik.io/providers/saml/groups') $provider = $null switch ($providerType) { 'OAuth2' { $redirect = & $substitute $row.RedirectUri $mode = if ($redirect -match '[\*\[\]\(\)\$\^]') { 'regex' } else { 'strict' } $provider = Invoke-AuthentikRequest -Method POST -Path '/providers/oauth2/' -Connection $connection -Body @{ name = $providerName authorization_flow = $authorization invalidation_flow = $invalidation client_type = $row.ClientType redirect_uris = @(@{ matching_mode = $mode; url = $redirect }) property_mappings = [object[]]@(Get-AuthentikManagedMapping -Kind Scope -Managed $openId -Connection $connection) } } 'Proxy' { # internal_host is optional in the schema and required by the # server in proxy mode: 'Internal host cannot be empty when forward # auth is disabled'. It is the backend the proxy fronts, and stays # as written because it is never a user-facing address. $provider = Invoke-AuthentikRequest -Method POST -Path '/providers/proxy/' -Connection $connection -Body @{ name = $providerName authorization_flow = $authorization invalidation_flow = $invalidation external_host = (& $substitute $row.ExternalHost) internal_host = $row.InternalHost mode = 'proxy' property_mappings = [object[]]@(Get-AuthentikManagedMapping -Kind Scope -Managed ($openId + 'goauthentik.io/providers/proxy/scope-proxy') -Connection $connection) } } 'SAML' { # Signed responses need a keypair the seed owns; borrowing the # instance's would sign lab assertions with a real key and leave # teardown nothing it could remove. $body = @{ name = $providerName authorization_flow = $authorization invalidation_flow = $invalidation signing_kp = (Get-AuthentikSigningKeypair -Connection $connection) property_mappings = [object[]]@(Get-AuthentikManagedMapping -Kind Saml -Managed $samlDefaults -Connection $connection) } foreach ($key in $settings.Keys) { $body[$key] = $settings[$key] } $provider = Invoke-AuthentikRequest -Method POST -Path '/providers/saml/' -Connection $connection -Body $body } 'LDAP' { $body = @{ name = $providerName authorization_flow = $authorization invalidation_flow = $invalidation } foreach ($key in $settings.Keys) { $body[$key] = $settings[$key] } $provider = Invoke-AuthentikRequest -Method POST -Path '/providers/ldap/' -Connection $connection -Body $body } 'RADIUS' { # The shared secret is generated here and never written anywhere: # a secret in a CSV is a secret in a repository. $body = @{ name = $providerName authorization_flow = $authorization invalidation_flow = $invalidation shared_secret = (New-TestPassword) } foreach ($key in $settings.Keys) { $body[$key] = if ($key -eq 'client_networks' -and $settings[$key] -is [array]) { $settings[$key] -join ',' } else { $settings[$key] } } $provider = Invoke-AuthentikRequest -Method POST -Path '/providers/radius/' -Connection $connection -Body $body } default { throw "Unknown provider type '$providerType' for '$($row.Name)'." } } $providerPk = [int]$provider.pk $result.ProvidersCreated++ Write-Verbose "Created $providerType provider $providerName" } } $body = @{ name = $name slug = $slug provider = $providerPk meta_launch_url = (& $substitute $row.LaunchUrl) meta_description = '{0} {1}' -f $row.Description, $marker.Marker group = $row.Group meta_hide = ($row.Hidden -eq 'TRUE') policy_engine_mode = 'any' } if (-not $body.meta_launch_url) { $body.Remove('meta_launch_url') } $application = $null if ($existingBySlug.ContainsKey($slug)) { $application = Invoke-AuthentikRequest -Method PATCH -Path "/core/applications/$slug/" -Body $body -Connection $connection $result.UpdatedApplications++ Write-Verbose "Updated application $name" } else { $application = Invoke-AuthentikRequest -Method POST -Path '/core/applications/' -Body $body -Connection $connection $result.CreatedApplications++ Write-Verbose "Created application $name" } $applications.Add([PSCustomObject]@{ Id = [string]$application.pk PbmUuid = [string]$application.pbm_uuid Key = $row.Name Name = $name Slug = $slug ProviderType = $providerType ProviderId = $providerPk Hidden = ($row.Hidden -eq 'TRUE') }) } catch { $message = "Failed to create application '$name': $($_.Exception.Message)" $result.Errors += $message Write-Error $message } } $result.Applications = $applications.ToArray() Write-Verbose ("Applications: $($result.CreatedApplications) created, $($result.UpdatedApplications) updated, " + "$($result.ProvidersCreated) providers, $($result.Errors.Count) problems") if ($PassThru) { return $result } } |