Private/AD/Get-ADUsersToResetPassword.ps1

<#
.SYNOPSIS
Build the Set-ADAccountPassword reset list for a set of AD users from Keysmith passphrases.

.DESCRIPTION
Decide-phase helper for Reset-IDBridgeADPassword. Requests a deterministic passphrase for every
user's SamAccountName via New-Passphrase — one batched call per 500 users, the Keysmith
server-side maximum per request — and builds a Set-ADAccountPassword -Reset splat for each.
Phrases come back in request order, so each user is paired with their phrase by position; an API
failure or a returned-count mismatch throws, since a reset with missing passphrases must not
proceed. The plaintext phrase rides along on each item for the caller's optional export and is
never logged here.

.PARAMETER UserList
The AD user objects to reset. Each needs SamAccountName and DistinguishedName.

.PARAMETER PassphraseAPI
Hashtable with Nonce (SecureString), AuthToken (SecureString), Mode, WordCount, and optional
Rev — the same shape as a source record's ADPassphraseAPI block, forwarded to New-Passphrase.

.PARAMETER ExportOnly
The caller is only exporting the passphrases, not resetting — the per-user proposed line logs
"Export passphrase" instead of "Reset password". Output is identical either way.

.OUTPUTS
[object[]] of @{ SamAccountName; GivenName; Surname; DistinguishedName; Passphrase; Splat }
where Splat is the Set-ADAccountPassword parameter hashtable.

.EXAMPLE
$resets = Get-ADUsersToResetPassword -UserList $users -PassphraseAPI $api

.NOTES
   Created by: Sam Cattanach
   Modified: 2026-08-31
#>

function Get-ADUsersToResetPassword {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        $UserList,

        [Parameter(Mandatory = $true)]
        [hashtable]$PassphraseAPI,

        [switch]$ExportOnly
    )

    $UserList = @($UserList)

    #One batched API call per 500 users - the Keysmith server-side max per request
    $phrases = @()
    for ($offset = 0; $offset -lt $UserList.Count; $offset += 500) {
        $chunk = @($UserList[$offset..([Math]::Min($offset + 499, $UserList.Count - 1))])

        try {
            $passphraseParams = @{
                Nonce = $PassphraseAPI.Nonce
                Username = @($chunk.SamAccountName)
                Mode = $PassphraseAPI.Mode
                WordCount = $PassphraseAPI.WordCount
                AuthToken = $PassphraseAPI.AuthToken
            }
            if ($PassphraseAPI.Rev) { $passphraseParams.Rev = $PassphraseAPI.Rev }

            $phrases += @(New-Passphrase @passphraseParams)
        }
        catch {
            Write-Log -Message ("AD: Password reset aborted. No passwords were changed. Password API Error $($_)") -Level "Error"
            Throw $_
        }
    }

    if (@($phrases).Count -ne $UserList.Count) {
        Write-Log -Message ("AD: Password reset aborted. No passwords were changed. Keysmith returned $(@($phrases).Count) passphrase(s) for $($UserList.Count) user(s).") -Level "Error"
        Throw "Keysmith returned $(@($phrases).Count) passphrase(s) for $($UserList.Count) user(s)."
    }

    $itemList = @()

    $proposedAction = if ($ExportOnly) { "Export passphrase" } else { "Reset password" }

    for ($i = 0; $i -lt $UserList.Count; $i++) {
        $item = $UserList[$i]

        Write-Log -Message ("AD: Proposed: $proposedAction for $($item.SamAccountName) ($($item.DistinguishedName)).")

        $itemList += [PSCustomObject]@{
            SamAccountName = $item.SamAccountName
            GivenName = $item.GivenName
            Surname = $item.Surname
            DistinguishedName = $item.DistinguishedName
            Passphrase = $phrases[$i]
            Splat = @{
                Identity = $item.DistinguishedName
                Reset = $true
                NewPassword = (ConvertTo-SecureString $phrases[$i] -AsPlainText -Force)
                ErrorAction = "Stop"
            }
        }
    }

    return $itemList
}