Providers/PingOne/Public/New-PingOneApplication.ps1

function New-PingOneApplication {
    <#
    .EXTERNALHELP TestEnvironment-Help.xml
    .SYNOPSIS
        Creates the seeded applications across every protocol and grants them their resource scopes
    #>


    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    [OutputType([PSCustomObject])]
    param(
        [Parameter()]
        [string[]]$Key,

        [Parameter()]
        [switch]$PassThru
    )

    $connection = Get-PingOneConnection
    $marker = Get-PingOneSeedMarker -Prefix $connection.Prefix
    $dataPath = Get-PingOneDataPath

    $rows = @(Import-Csv -LiteralPath (Join-Path $dataPath 'PingOneApplications.csv') -Encoding UTF8)
    if ($Key) { $rows = @($rows | Where-Object { $Key -contains $_.Key }) }

    # Groups and resources, by the name the seed gave them.
    $groupNameByKey = @{}
    foreach ($groupRow in (Import-Csv -LiteralPath (Join-Path $dataPath 'PingOneGroups.csv') -Encoding UTF8)) {
        $groupNameByKey[$groupRow.Key] = Resolve-PingOneSeedName -Key $groupRow.Name -Kind DisplayName -Connection $connection
    }
    $groupByName = @{}
    foreach ($group in (Invoke-PingOneRequest -Method GET -Path 'groups' -Paginate -Connection $connection)) {
        $groupByName[[string]$group.name] = $group
    }

    $resourceNameByKey = @{}
    foreach ($resourceRow in (Import-Csv -LiteralPath (Join-Path $dataPath 'PingOneResources.csv') -Encoding UTF8)) {
        $resourceNameByKey[$resourceRow.Key] = Resolve-PingOneSeedName -Key $resourceRow.Name -Kind DisplayName -Connection $connection
    }
    $resourceByName = @{}
    foreach ($resource in (Invoke-PingOneRequest -Method GET -Path 'resources' -Paginate -Connection $connection)) {
        $resourceByName[[string]$resource.name] = $resource
    }

    $existing = @{}
    foreach ($application in (Invoke-PingOneRequest -Method GET -Path 'applications' -Paginate -Connection $connection)) {
        $existing[[string]$application.name] = $application
    }

    $created = [System.Collections.Generic.List[object]]::new()
    $reused = [System.Collections.Generic.List[object]]::new()
    $errors = [System.Collections.Generic.List[string]]::new()
    $grantsApplied = 0

    $split = { param($value) @(([string]$value -split ';') | Where-Object { $_ }) }
    $expand = { param($value) ([string]$value).Replace('{domain}', $connection.EmailDomain) }

    foreach ($row in $rows) {
        $name = Resolve-PingOneSeedName -Key $row.Name -Kind DisplayName -Connection $connection
        $application = $null

        if ($existing.ContainsKey($name)) {
            Write-Verbose "Application $name already exists; reusing"
            $application = $existing[$name]
            $reused.Add([PSCustomObject]@{ Key = $row.Key; Name = $name; Id = $application.id })
        }
        else {
            if (-not $PSCmdlet.ShouldProcess($name, 'Create PingOne application')) { continue }

            $body = @{
                name        = $name
                description = '{0} {1}' -f $row.Purpose, $marker.Tag
                enabled     = [bool]::Parse($row.Enabled)
                type        = $row.Type
                protocol    = $row.Protocol
            }

            if ($row.Protocol -eq 'SAML') {
                $body['spEntityId'] = & $expand $row.SpEntityId
                $body['acsUrls'] = @(& $split (& $expand $row.AcsUrls))
                $body['assertionDuration'] = 3600
            }
            else {
                $body['grantTypes'] = @(& $split $row.GrantTypes)
                $body['responseTypes'] = @(& $split $row.ResponseTypes)
                $body['tokenEndpointAuthMethod'] = $row.TokenAuth
                $body['redirectUris'] = @(& $split (& $expand $row.RedirectUris))

                # A public client runs safely only with PKCE, so it is never seeded without it.
                if ($row.TokenAuth -eq 'NONE') { $body['pkceEnforcement'] = 'S256_REQUIRED' }
            }

            $assignedGroupIds = foreach ($groupKey in (& $split $row.AssignGroups)) {
                $groupName = $groupNameByKey[$groupKey]
                if ($groupName -and $groupByName.ContainsKey($groupName)) {
                    @{ id = $groupByName[$groupName].id }
                }
                else {
                    $errors.Add("Group '$groupKey' for application $name does not exist; run New-PingOneGroup first")
                }
            }
            if (@($assignedGroupIds).Count -gt 0) {
                $body['accessControl'] = @{ group = @{ type = 'ANY_GROUP'; groups = @($assignedGroupIds) } }
            }

            try {
                $application = Invoke-PingOneRequest -Method POST -Path 'applications' -Body $body -Connection $connection
                $created.Add([PSCustomObject]@{ Key = $row.Key; Name = $name; Id = $application.id; Protocol = $row.Protocol })
                Write-Verbose "Created application $name"
            }
            catch {
                $errors.Add("Could not create application ${name}: $($_.Exception.Message)")
                Write-Warning "Could not create application ${name}: $($_.Exception.Message)"
                continue
            }
        }

        # Grants, one per resource. SAML applications are granted nothing, because they are
        # not issued access tokens.
        if ($row.Protocol -eq 'SAML') { continue }

        $scopesByResource = @{}
        foreach ($entry in (& $split $row.Scopes)) {
            $resourceKey, $scopeName = $entry -split ':', 2
            if (-not $scopesByResource.ContainsKey($resourceKey)) { $scopesByResource[$resourceKey] = @() }
            $scopesByResource[$resourceKey] += $scopeName
        }
        if ($scopesByResource.Count -eq 0) { continue }

        $existingGrants = @(Invoke-PingOneRequest -Method GET -Path "applications/$($application.id)/grants" -Paginate -Connection $connection)

        foreach ($resourceKey in $scopesByResource.Keys) {
            $resourceName = $resourceNameByKey[$resourceKey]
            $resource = $null
            if ($resourceName) { $resource = $resourceByName[$resourceName] }
            if (-not $resource) {
                $errors.Add("Resource '$resourceKey' for application $name does not exist; run New-PingOneResource first")
                continue
            }

            if (@($existingGrants | Where-Object { $_.resource.id -eq $resource.id })) { continue }

            $scopeObjects = @(Invoke-PingOneRequest -Method GET -Path "resources/$($resource.id)/scopes" -Paginate -Connection $connection)
            $wanted = foreach ($scopeName in $scopesByResource[$resourceKey]) {
                $match = @($scopeObjects | Where-Object { $_.name -eq $scopeName })
                if ($match) { @{ id = $match[0].id } }
                else { $errors.Add("Scope '$scopeName' on $resourceName does not exist for application $name") }
            }
            if (@($wanted).Count -eq 0) { continue }

            if (-not $PSCmdlet.ShouldProcess("$name -> $resourceName", 'Grant PingOne resource scopes')) { continue }

            try {
                $null = Invoke-PingOneRequest -Method POST -Path "applications/$($application.id)/grants" `
                    -Body @{ resource = @{ id = $resource.id }; scopes = @($wanted) } -Connection $connection
                $grantsApplied++
            }
            catch {
                $errors.Add("Could not grant $resourceName to ${name}: $($_.Exception.Message)")
            }
        }
    }

    if ($PassThru) {
        return [PSCustomObject]@{
            TotalApplications   = @($rows).Count
            CreatedApplications = $created.Count
            ReusedApplications  = $reused.Count
            GrantsApplied       = $grantsApplied
            Applications        = (@($created) + @($reused))
            Errors              = $errors.ToArray()
        }
    }
}