Providers/FreeIPA/Public/New-FreeIPAEnvironment.ps1

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

    .DESCRIPTION
        Runs the component functions in the only order that works: groups before the users
        that join them, host groups before the hosts that join them, and then the access
        layers that name all four - netgroups, HBAC rules, sudo rules, roles, password
        policies and services - each of which needs the directory to exist, and last the
        identity detail: ID views over users and hosts, tokens on users, automember rules
        rebuilt against the seeded entries, the automount location, SELinux maps over the
        HBAC rules, and certificate mapping. 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 host by name rather than saying only that a step would run.

    .PARAMETER Skip
        Steps to leave out: Groups, IdentityProviders, Users, Hostgroups, Dns, Hosts, Netgroups, HbacRules, SudoRules,
        Roles, PasswordPolicies, Services, IdViews, OtpTokens, AutomemberRules, Automount,
        SelinuxUserMaps, CertMapRules, CaAcls, Certificates.

    .PARAMETER AccountPassword
        A password to set on the seeded users whose row asks for one. Without it nobody can
        log in.

    .PARAMETER ShowProgress
        Show a progress bar through the long steps, 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-FreeIPAEnvironment

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

    .EXAMPLE
        PS> New-FreeIPAEnvironment -Skip Hosts, Hostgroups -PassThru

        DESCRIPTION: Seeds the people and groups only
        OUTPUT: The result object with two attempted steps
        USE CASE: A realm where the host inventory is not wanted

    .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', 'IdentityProviders', 'Users', 'Hostgroups', 'Dns', 'Hosts', 'Netgroups', 'HbacRules', 'SudoRules', 'Roles',
            'PasswordPolicies', 'Services', 'IdViews', 'OtpTokens', 'AutomemberRules', 'Automount', 'SelinuxUserMaps',
            'CertMapRules', 'CaAcls', 'Certificates')]
        [string[]]$Skip = @(),

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

        [Parameter()]
        [switch]$ShowProgress,

        [Parameter()]
        [switch]$PassThru
    )

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

        $connection = Get-FreeIPAConnection

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

    process {
        Write-TestMessage -Message "FreeIPA 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 }
                IdentityProviders = @{ Attempted = $false; Success = $false; Results = $null }
                Users      = @{ Attempted = $false; Success = $false; Results = $null }
                Hostgroups       = @{ Attempted = $false; Success = $false; Results = $null }
                Dns              = @{ Attempted = $false; Success = $false; Results = $null }
                Hosts            = @{ Attempted = $false; Success = $false; Results = $null }
                Netgroups        = @{ Attempted = $false; Success = $false; Results = $null }
                HbacRules        = @{ Attempted = $false; Success = $false; Results = $null }
                SudoRules        = @{ Attempted = $false; Success = $false; Results = $null }
                Roles            = @{ Attempted = $false; Success = $false; Results = $null }
                PasswordPolicies = @{ Attempted = $false; Success = $false; Results = $null }
                Services         = @{ Attempted = $false; Success = $false; Results = $null }
                IdViews          = @{ Attempted = $false; Success = $false; Results = $null }
                OtpTokens        = @{ Attempted = $false; Success = $false; Results = $null }
                AutomemberRules  = @{ Attempted = $false; Success = $false; Results = $null }
                Automount        = @{ Attempted = $false; Success = $false; Results = $null }
                SelinuxUserMaps  = @{ Attempted = $false; Success = $false; Results = $null }
                CertMapRules     = @{ Attempted = $false; Success = $false; Results = $null }
                CaAcls           = @{ Attempted = $false; Success = $false; Results = $null }
                Certificates     = @{ Attempted = $false; Success = $false; Results = $null }
            }
            Summary       = [ordered]@{
                TotalOperations      = 0
                SuccessfulOperations = 0
                FailedOperations     = 0
            }
        }

        $stepArgs = @{ 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-FreeIPAGroup @stepArgs }
                Report = { param($r) "$($r.CreatedGroups) created, $($r.UpdatedGroups) updated, $($r.NestingsApplied) nestings" }
            }
            @{
                Key    = 'IdentityProviders'
                Title  = 'Step 2: Creating RADIUS proxies and identity providers'
                Run    = { New-FreeIPAIdentityProvider -PassThru -Confirm:$false }
                Report = { param($r) "$($r.CreatedProxies) proxies, $($r.CreatedIdps) providers" }
            }
            @{
                Key    = 'Users'
                Title  = 'Step 3: Creating users'
                Run    = { New-FreeIPAUser @userArgs }
                Report = { param($r) "$($r.CreatedUsers) created, $($r.UpdatedUsers) updated, $($r.MembershipsApplied) memberships" }
            }
            @{
                Key    = 'Hostgroups'
                Title  = 'Step 4: Creating host groups'
                Run    = { New-FreeIPAHostgroup @stepArgs }
                Report = { param($r) "$($r.CreatedHostgroups) created, $($r.UpdatedHostgroups) updated, $($r.NestingsApplied) nestings" }
            }
            @{
                Key    = 'Dns'
                Title  = 'Step 5: Creating the DNS zones and records'
                Run    = { New-FreeIPADnsZone -PassThru -Confirm:$false }
                Report = { param($r) "$($r.ZonesCreated) zones, $($r.RecordsCreated) records" }
            }
            @{
                Key    = 'Hosts'
                Title  = 'Step 6: Creating hosts'
                Run    = { New-FreeIPAHost @stepArgs }
                Report = { param($r) "$($r.CreatedHosts) created, $($r.UpdatedHosts) updated, $($r.MembershipsApplied) memberships" }
            }
            @{
                Key    = 'Netgroups'
                Title  = 'Step 7: Creating netgroups'
                Run    = { New-FreeIPANetgroup -PassThru -Confirm:$false }
                Report = { param($r) "$($r.CreatedNetgroups) created, $($r.MembershipsApplied) memberships" }
            }
            @{
                Key    = 'HbacRules'
                Title  = 'Step 8: Creating HBAC services and rules'
                Run    = { New-FreeIPAHbacRule -PassThru -Confirm:$false }
                Report = { param($r) "$($r.CreatedRules) rules, $($r.ServicesCreated) services, $($r.ServiceGroupsCreated) service groups" }
            }
            @{
                Key    = 'SudoRules'
                Title  = 'Step 9: Creating sudo commands and rules'
                Run    = { New-FreeIPASudoRule -PassThru -Confirm:$false }
                Report = { param($r) "$($r.CreatedRules) rules, $($r.CommandsCreated) commands, $($r.CommandsReused) reused" }
            }
            @{
                Key    = 'Roles'
                Title  = 'Step 10: Creating permissions, privileges and roles'
                Run    = { New-FreeIPARole -PassThru -Confirm:$false }
                Report = { param($r) "$($r.CreatedRoles) roles, $($r.PrivilegesCreated) privileges, $($r.PermissionsCreated) permissions" }
            }
            @{
                Key    = 'PasswordPolicies'
                Title  = 'Step 11: Creating password policies'
                Run    = { New-FreeIPAPasswordPolicy -PassThru -Confirm:$false }
                Report = { param($r) "$($r.CreatedPolicies) created, $($r.UpdatedPolicies) updated" }
            }
            @{
                Key    = 'Services'
                Title  = 'Step 12: Creating services and delegation rules'
                Run    = { New-FreeIPAService -PassThru -Confirm:$false }
                Report = { param($r) "$($r.CreatedServices) services, $($r.DelegationRulesCreated) rules, $($r.DelegationTargetsCreated) targets" }
            }
            @{
                Key    = 'IdViews'
                Title  = 'Step 13: Creating ID views and overrides'
                Run    = { New-FreeIPAIdView -PassThru -Confirm:$false }
                Report = { param($r) "$($r.CreatedViews) views, $($r.OverridesCreated) overrides, $($r.HostsApplied) applied" }
            }
            @{
                Key    = 'OtpTokens'
                Title  = 'Step 14: Creating OTP tokens'
                Run    = { New-FreeIPAOtpToken -PassThru -Confirm:$false }
                Report = { param($r) "$($r.CreatedTokens) created, $($r.UpdatedTokens) updated" }
            }
            @{
                Key    = 'AutomemberRules'
                Title  = 'Step 15: Creating automember rules and rebuilding the seeded entries'
                Run    = { New-FreeIPAAutomemberRule -PassThru -Confirm:$false }
                Report = { param($r) "$($r.CreatedRules) rules, $($r.ConditionsApplied) conditions, $($r.EntriesRebuilt) entries rebuilt" }
            }
            @{
                Key    = 'Automount'
                Title  = 'Step 16: Creating the automount location, maps and keys'
                Run    = { New-FreeIPAAutomount -PassThru -Confirm:$false }
                Report = { param($r) "$($r.LocationsCreated) locations, $($r.MapsCreated) maps, $($r.KeysCreated) keys" }
            }
            @{
                Key    = 'SelinuxUserMaps'
                Title  = 'Step 17: Creating SELinux user maps'
                Run    = { New-FreeIPASelinuxUserMap -PassThru -Confirm:$false }
                Report = { param($r) "$($r.CreatedMaps) created, $($r.MembershipsApplied) memberships" }
            }
            @{
                Key    = 'CertMapRules'
                Title  = 'Step 18: Creating certificate mapping rules'
                Run    = { New-FreeIPACertMapRule -PassThru -Confirm:$false }
                Report = { param($r) "$($r.CreatedRules) created, $($r.UpdatedRules) updated" }
            }
            @{
                Key    = 'CaAcls'
                Title  = 'Step 19: Creating CA ACLs'
                Run    = { New-FreeIPACaAcl -PassThru -Confirm:$false }
                Report = { param($r) "$($r.CreatedAcls) created, $($r.MembershipsApplied) memberships" }
            }
            @{
                Key    = 'Certificates'
                Title  = 'Step 20: Issuing certificates from the realm CA'
                Run    = { New-FreeIPACertificate -PassThru -Confirm:$false }
                Report = { param($r) "$($r.Issued) issued, $($r.Revoked) revoked, $($r.Existing) already there" }
            }
        )

        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-FreeIPAEnvironment - CorrelationId: $correlationId"
    }
}