Providers/Authentik/Public/New-AuthentikEnvironment.ps1

function New-AuthentikEnvironment {
    <#
    .SYNOPSIS
        Seeds the complete Authentik test environment in dependency order

    .DESCRIPTION
        Runs the thirteen component functions in the only order that works: groups before the
        users that join them and the roles assigned to them, applications before the outposts
        that carry their providers, the flows attached to them and the scope mappings,
        entitlements and policies that attach to them, notification rules before the bindings
        that make them fire, and tokens and invitations last because only the invitations
        depend on anything, and that is the flows. Each step is attempted, recorded and followed by the next, so one
        failing step does not abandon the rest.

        A step that returns normally has not necessarily worked. Every component collects what
        it could not do into an Errors property rather than throwing on the first bad row, so
        the orchestrator reads that property and counts a step with errors as failed. Counting
        only thrown exceptions is how an earlier provider reported success over a screen of
        failures.

        There is no ShouldProcess gate at this level. Each step runs its own, and
        $WhatIfPreference reaches into them, which is what makes -WhatIf list every group,
        user and application by name rather than saying only that a step would run.

    .PARAMETER Skip
        Steps to leave out: Groups, Users, Roles, Applications, Outposts, Flows, ScopeMappings,
        Entitlements, Policies, NotificationRules, Bindings, Tokens, Invitations.

    .PARAMETER AccountPassword
        A password to set on every seeded user. Without it they cannot sign in.

    .PARAMETER ShowProgress
        Show a progress bar through the groups and users steps, which are the long ones at
        three hundred users, and report each step's counts as it completes.

    .PARAMETER PassThru
        Returns the result object.

    .OUTPUTS
        PSCustomObject with CorrelationId, BaseUrl, Prefix, StartTime, EndTime, Duration,
        Operations and Summary.

    .EXAMPLE
        PS> New-AuthentikEnvironment

        DESCRIPTION: Seeds everything
        OUTPUT: A summary line per step and a closing verdict
        USE CASE: Called by New-TestEnvironment when Authentik is the active provider

    .EXAMPLE
        PS> New-AuthentikEnvironment -Skip Policies, NotificationRules -PassThru

        DESCRIPTION: Seeds the directory objects only
        OUTPUT: The result object with three attempted steps
        USE CASE: An instance where policies would govern real sign-ins

    .NOTES
        Author: Jeffrey Stuhr
        Blog: https://www.techbyjeff.net
        LinkedIn: https://www.linkedin.com/in/jeffrey-stuhr-034214aa/
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '',
        Justification = 'The step summary is written for the person watching the seed run; the result object carries the same data for scripts.')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '',
        Justification = 'Delegated to the step functions, which each call ShouldProcess per object.')]
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    [OutputType([PSCustomObject])]
    param(
        [Parameter()]
        [ValidateSet('Groups', 'Users', 'Roles', 'Applications', 'Outposts', 'Flows', 'ScopeMappings', 'Entitlements',
            'Policies', 'NotificationRules', 'Bindings', 'Tokens', 'Invitations')]
        [string[]]$Skip = @(),

        [Parameter()]
        [System.Security.SecureString]$AccountPassword,

        [Parameter()]
        [switch]$ShowProgress,

        [Parameter()]
        [switch]$PassThru
    )

    begin {
        $correlationId = [Guid]::NewGuid()
        Write-Verbose "Starting New-AuthentikEnvironment - CorrelationId: $correlationId"

        $connection = Get-AuthentikConnection

        if (-not (Test-AuthentikPrerequisite -CheckDataFiles)) {
            throw 'Prerequisites not met for Authentik test environment creation'
        }
    }

    process {
        Write-TestMessage -Message "Authentik Test Environment Creation ($($connection.BaseUrl))" -Type Header

        $results = [PSCustomObject]@{
            CorrelationId = $correlationId
            BaseUrl       = $connection.BaseUrl
            Prefix        = $connection.Prefix
            StartTime     = Get-Date
            EndTime       = $null
            Duration      = $null
            Operations    = [ordered]@{
                Groups            = @{ Attempted = $false; Success = $false; Results = $null }
                Users             = @{ Attempted = $false; Success = $false; Results = $null }
                Roles             = @{ Attempted = $false; Success = $false; Results = $null }
                Applications      = @{ Attempted = $false; Success = $false; Results = $null }
                Outposts          = @{ Attempted = $false; Success = $false; Results = $null }
                Flows             = @{ Attempted = $false; Success = $false; Results = $null }
                ScopeMappings     = @{ Attempted = $false; Success = $false; Results = $null }
                Entitlements      = @{ Attempted = $false; Success = $false; Results = $null }
                Policies          = @{ Attempted = $false; Success = $false; Results = $null }
                NotificationRules = @{ Attempted = $false; Success = $false; Results = $null }
                Bindings          = @{ Attempted = $false; Success = $false; Results = $null }
                Tokens            = @{ Attempted = $false; Success = $false; Results = $null }
                Invitations       = @{ Attempted = $false; Success = $false; Results = $null }
            }
            Summary       = [ordered]@{
                TotalOperations      = 0
                SuccessfulOperations = 0
                FailedOperations     = 0
            }
        }

        $groupArgs = @{ PassThru = $true; Confirm = $false; ShowProgress = $ShowProgress }
        $userArgs = @{ PassThru = $true; Confirm = $false; ShowProgress = $ShowProgress }
        if ($AccountPassword) { $userArgs['AccountPassword'] = $AccountPassword }

        $steps = @(
            @{
                Key    = 'Groups'
                Title  = 'Step 1: Creating groups'
                Run    = { New-AuthentikGroup @groupArgs }
                Report = { param($r) "$($r.CreatedGroups) created, $($r.UpdatedGroups) updated" }
            }
            @{
                Key    = 'Users'
                Title  = 'Step 2: Creating users'
                Run    = { New-AuthentikUser @userArgs }
                Report = { param($r) "$($r.CreatedUsers) created, $($r.UpdatedUsers) updated" }
            }
            @{
                Key    = 'Roles'
                Title  = 'Step 3: Creating roles and assigning them to groups'
                Run    = { New-AuthentikRole -PassThru -Confirm:$false }
                Report = { param($r) "$($r.CreatedRoles) created, $($r.GroupsAssigned) group assignments" }
            }
            @{
                Key    = 'Applications'
                Title  = 'Step 4: Creating applications and providers'
                Run    = { New-AuthentikApplication -PassThru -Confirm:$false }
                Report = { param($r) "$($r.CreatedApplications) created, $($r.ProvidersCreated) providers" }
            }
            @{
                Key    = 'Outposts'
                Title  = 'Step 5: Creating outposts for the providers that serve through one'
                Run    = { New-AuthentikOutpost -PassThru -Confirm:$false }
                Report = { param($r) "$($r.CreatedOutposts) created" }
            }
            @{
                Key    = 'Flows'
                Title  = 'Step 6: Creating stages and flows and attaching them to providers'
                Run    = { New-AuthentikFlow -PassThru -Confirm:$false }
                Report = { param($r) "$($r.CreatedFlows) flows, $($r.StagesCreated) stages, $($r.ProvidersUpdated) providers updated" }
            }
            @{
                Key    = 'ScopeMappings'
                Title  = 'Step 7: Creating scope mappings and attaching them to providers'
                Run    = { New-AuthentikScopeMapping -PassThru -Confirm:$false }
                Report = { param($r) "$($r.CreatedMappings) created, $($r.ProvidersUpdated) providers updated" }
            }
            @{
                Key    = 'Entitlements'
                Title  = 'Step 8: Creating application entitlements'
                Run    = { New-AuthentikEntitlement -PassThru -Confirm:$false }
                Report = { param($r) "$($r.CreatedEntitlements) created" }
            }
            @{
                Key    = 'Policies'
                Title  = 'Step 9: Creating policies and their application bindings'
                Run    = { New-AuthentikPolicy -PassThru -Confirm:$false }
                Report = { param($r) "$($r.CreatedPolicies) created, $($r.BindingsCreated) bindings" }
            }
            @{
                Key    = 'NotificationRules'
                Title  = 'Step 10: Creating notification rules'
                Run    = { New-AuthentikNotificationRule -PassThru -Confirm:$false }
                Report = { param($r) "$($r.CreatedRules) created, $($r.TransportsCreated) transports" }
            }
            @{
                Key    = 'Bindings'
                Title  = 'Step 11: Creating group, user and policy bindings'
                Run    = { New-AuthentikBinding -PassThru -Confirm:$false }
                Report = { param($r) "$($r.CreatedBindings) created, $($r.ExistingBindings) existing" }
            }
            @{
                Key    = 'Tokens'
                Title  = 'Step 12: Creating user tokens'
                Run    = { New-AuthentikToken -PassThru -Confirm:$false }
                Report = { param($r) "$($r.CreatedTokens) created" }
            }
            @{
                Key    = 'Invitations'
                Title  = 'Step 13: Creating invitations'
                Run    = { New-AuthentikInvitation -PassThru -Confirm:$false }
                Report = { param($r) "$($r.CreatedInvitations) created" }
            }
        )

        foreach ($step in $steps) {
            if ($step.Key -in $Skip) {
                Write-TestMessage -Message "$($step.Title) - skipped as requested" -Type Warning
                continue
            }

            Write-TestMessage -Message $step.Title -Type Info
            $results.Operations[$step.Key].Attempted = $true
            $results.Summary.TotalOperations++

            try {
                $stepResult = & $step.Run
                $results.Operations[$step.Key].Results = $stepResult

                $stepErrors = @()
                if ($stepResult -and ($stepResult.PSObject.Properties.Name -contains 'Errors')) {
                    $stepErrors = @($stepResult.Errors)
                }

                if ($stepErrors.Count -gt 0) {
                    $results.Operations[$step.Key].Success = $false
                    $results.Summary.FailedOperations++
                    foreach ($stepError in $stepErrors) {
                        Write-Error "$($step.Title): $stepError"
                    }
                }
                else {
                    $results.Operations[$step.Key].Success = $true
                    $results.Summary.SuccessfulOperations++
                }

                if ($ShowProgress -and $stepResult) {
                    Write-Verbose (& $step.Report $stepResult)
                }
            }
            catch {
                $results.Operations[$step.Key].Results = $_.Exception.Message
                $results.Summary.FailedOperations++
                Write-Error "$($step.Title) failed: $($_.Exception.Message)"
            }
        }

        $results.EndTime = Get-Date
        $results.Duration = $results.EndTime - $results.StartTime

        Write-TestMessage -Message 'Environment Creation Summary' -Type Header
        Write-Host ("Operations completed: $($results.Summary.SuccessfulOperations)/" +
            "$($results.Summary.TotalOperations)") -ForegroundColor Green
        Write-Host "Duration: $($results.Duration.ToString('hh\:mm\:ss'))" -ForegroundColor Green

        if ($results.Summary.FailedOperations -gt 0) {
            Write-Warning "Failed operations: $($results.Summary.FailedOperations)"
            Write-Warning 'Inspect the results object with -PassThru for the detail.'
            Write-TestMessage -Message ('Test environment creation finished with ' +
                "$($results.Summary.FailedOperations) failed step(s).") -Type Error
        }
        else {
            Write-TestMessage -Message 'Test environment creation complete.' -Type Success
        }

        if ($PassThru) { return $results }
    }

    end {
        Write-Verbose "Completed New-AuthentikEnvironment - CorrelationId: $correlationId"
    }
}