Providers/Okta/Public/New-OktaUser.ps1

function New-OktaUser {
    <#
    .EXTERNALHELP TestEnvironment-Help.xml
    .SYNOPSIS
        Creates the seeded Okta users from Data\OktaUsers.csv
    #>


    # The justification has to be one string constant: PSScriptAnalyzer rejects a concatenation
    # in a suppression attribute, and the error it gives says nothing about which one.
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '',
        Justification = 'Deliberately weak shared lab password; -AccountPassword overrides it.')]
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    [OutputType([PSCustomObject])]
    param(
        [Parameter()]
        [ValidateRange(1, 8)]
        [int]$UserCount = 8,

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

        [Parameter()]
        [switch]$SkipLifecycleStates,

        [Parameter()]
        [switch]$PassThru
    )

    $connection = Get-OktaConnection

    if (-not $AccountPassword) {
        $AccountPassword = ConvertTo-SecureString -String 'Okta-Lab-Passw0rd!2026' -AsPlainText -Force
    }
    $plainPassword = ConvertFrom-TestSecureString -SecureString $AccountPassword

    $csvPath = Join-Path -Path (Get-OktaDataPath) -ChildPath 'OktaUsers.csv'
    $rows = @(Import-Csv -Path $csvPath -Encoding UTF8 | Select-Object -First $UserCount)

    $result = [PSCustomObject]@{
        TotalUsers   = $rows.Count
        CreatedUsers = 0
        UpdatedUsers = 0
        Users        = @()
        Errors       = @()
    }

    # Manager is a display name and managerId a login, so the whole file has to be in hand
    # before the first user is built, not just the first $UserCount rows.
    $allRows = @(Import-Csv -Path $csvPath -Encoding UTF8)
    $displayNameByLogin = @{}
    foreach ($row in $allRows) { $displayNameByLogin[$row.LoginPrefix] = $row.DisplayName }

    # Resolved once. A user's type can only be set at creation, so a row naming a type that
    # does not exist is worth failing loudly on rather than silently creating a default user.
    $userTypeIdByKey = @{}
    if (@($rows | Where-Object { $_.PSObject.Properties['OktaUserType'] -and $_.OktaUserType })) {
        foreach ($type in (Invoke-OktaRequest -Method GET -Path '/api/v1/meta/types/user')) {
            $userTypeIdByKey[$type.name] = $type.id
        }
    }

    $created = [System.Collections.Generic.List[object]]::new()

    foreach ($row in $rows) {
        $login = '{0}@{1}' -f $row.LoginPrefix, $connection.EmailDomain

        $userProfile = [ordered]@{
            login             = $login
            email             = $login
            firstName         = $row.FirstName
            lastName          = $row.LastName
            displayName       = $row.DisplayName
            nickName          = $row.NickName
            title             = $row.Title
            userType          = $row.UserType
            department        = $row.Department
            division          = $row.Division
            organization      = $row.Organization
            costCenter        = $row.CostCenter
            employeeNumber    = $row.EmployeeNumber
            mobilePhone       = $row.MobilePhone
            primaryPhone      = $row.PrimaryPhone
            streetAddress     = $row.StreetAddress
            city              = $row.City
            state             = $row.State
            zipCode           = $row.ZipCode
            countryCode       = $row.CountryCode
            preferredLanguage = $row.PreferredLanguage
            locale            = $row.Locale
            timezone          = $row.Timezone

            labSeedTag        = $connection.SeedTag
            labBadgeId        = $row.LabBadgeId
            labClearanceLevel = $row.LabClearanceLevel
            labIsContractor   = [bool]::Parse($row.LabIsContractor)
            labCostCenterOwner = $row.LabCostCenterOwner
        }

        if ($row.MiddleName)      { $userProfile.middleName = $row.MiddleName }
        if ($row.HonorificPrefix) { $userProfile.honorificPrefix = $row.HonorificPrefix }

        if ($row.ManagerLoginPrefix) {
            $userProfile.managerId = '{0}@{1}' -f $row.ManagerLoginPrefix, $connection.EmailDomain
            $userProfile.manager   = $displayNameByLogin[$row.ManagerLoginPrefix]
        }

        # Compared against empty string rather than tested for truth. A risk score of zero is
        # a legitimate value and falsy in PowerShell, and dropping it silently is exactly the
        # bug this attribute exists to expose.
        if ($row.LabRiskScore -ne '') { $userProfile.labRiskScore = [int]$row.LabRiskScore }
        if ($row.LabContractEndDate)  { $userProfile.labContractEndDate = $row.LabContractEndDate }

        $entitlements = @($row.LabEntitlements -split ';' | Where-Object { $_ })
        if ($entitlements.Count -gt 0) { $userProfile.labEntitlements = $entitlements }

        # Attributes that live only on the Contractor type's schema. Sending them to a default
        # user is a 400, so they follow the type rather than being set unconditionally.
        if ($row.PSObject.Properties['OktaUserType'] -and $row.OktaUserType) {
            if ($row.LabAgencyName)    { $userProfile.labAgencyName = $row.LabAgencyName }
            if ($row.LabPurchaseOrder) { $userProfile.labPurchaseOrder = $row.LabPurchaseOrder }
        }

        $lifecycle = if ($SkipLifecycleStates) { 'Active' } else { $row.LifecycleState }

        if (-not $PSCmdlet.ShouldProcess($login, "Create Okta user ($lifecycle)")) { continue }

        try {
            $existing = $null
            try {
                $existing = Invoke-OktaRequest -Method GET `
                    -Path "/api/v1/users/$([uri]::EscapeDataString($login))"
            }
            catch {
                Write-Verbose "No existing user for $login; it will be created."
            }

            if ($existing) {
                # A partial profile update. Okta merges rather than replaces here, which is
                # what makes a re-run after a half-finished seed safe.
                $user = Invoke-OktaRequest -Method POST -Path "/api/v1/users/$($existing.id)" `
                    -Body @{ profile = $userProfile }
                $result.UpdatedUsers++
                Write-Verbose "Updated $login"
            }
            else {
                $activate = ($lifecycle -ne 'Staged')
                $body = @{
                    profile     = $userProfile
                    credentials = @{ password = @{ value = $plainPassword } }
                }

                if ($row.PSObject.Properties['OktaUserType'] -and $row.OktaUserType) {
                    $typeName = Get-OktaUserTypeName -Prefix $connection.Prefix -UserTypeKey $row.OktaUserType
                    if (-not $userTypeIdByKey.ContainsKey($typeName)) {
                        throw ("User type '$typeName' does not exist. Run New-OktaUserType " +
                            'before creating users that belong to it.')
                    }
                    $body.type = @{ id = $userTypeIdByKey[$typeName] }
                }

                $user = Invoke-OktaRequest -Method POST -Path '/api/v1/users' `
                    -Query @{ activate = $activate.ToString().ToLowerInvariant() } -Body $body
                $result.CreatedUsers++
                Write-Verbose "Created $login ($lifecycle)"
            }

            if ($lifecycle -eq 'Suspended' -and $user.status -ne 'SUSPENDED') {
                $null = Invoke-OktaRequest -Method POST `
                    -Path "/api/v1/users/$($user.id)/lifecycle/suspend"
                $user.status = 'SUSPENDED'
            }

            $created.Add([PSCustomObject]@{
                Id          = $user.id
                Login       = $login
                DisplayName = $row.DisplayName
                Status      = $user.status
                Groups      = @($row.Groups -split ';' | Where-Object { $_ })
            })
        }
        catch {
            $message = "Failed to create $login`: $($_.Exception.Message)"
            $result.Errors += $message
            Write-Error $message
        }
    }

    $result.Users = $created.ToArray()

    Write-Verbose ("Users: $($result.CreatedUsers) created, $($result.UpdatedUsers) updated, " +
        "$($result.Errors.Count) failed")

    if ($PassThru) { return $result }
}