Providers/FreeIPA/Private/Set-FreeIPAPassword.ps1

function Set-FreeIPAPassword {
    <#
    .SYNOPSIS
        Changes a user's password as that user, through the endpoint that needs no session

    .DESCRIPTION
        Any password an administrator sets in FreeIPA is expired the moment it is set: the user
        has to change it before it can be used. The change-password endpoint is how that
        happens without a session - it takes the login, the old password and the new one - and
        it is the only way to end up with a password that is current rather than
        must-change. The seed uses it three times over: to make the service account's password
        current after the bootstrap set it, to rotate that password when the realm's policy
        has expired it, and to give a seeded user whose row says 'Current' a password that is.

        The result comes back in headers rather than a status: X-IPA-Pwchange-Result is 'ok',
        'invalid-password', 'policy-error' or 'error', and a policy failure explains itself in
        X-IPA-Pwchange-Policy-Error. Anything but 'ok' is thrown with that explanation, because
        a caller that carried on would then store or report a password that does not work.

    .PARAMETER Connection
        The connection whose client to send through. Only the client and base URL are used;
        the change is authenticated by the old password, not by the session.

    .PARAMETER Username
        The login whose password changes.

    .PARAMETER OldPassword
        The current password.

    .PARAMETER NewPassword
        The new password.

    .OUTPUTS
        None.

    .EXAMPLE
        PS> Set-FreeIPAPassword -Connection $connection -Username 'zz-test-automation' -OldPassword $random -NewPassword $fresh

        DESCRIPTION: Makes an admin-set password current
        OUTPUT: None; throws with the policy error if the realm refuses
        USE CASE: The bootstrap, right after creating the service account

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


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', 'OldPassword',
        Justification = 'Already in memory as text from the record or the API; sent to the server as a form field.')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', 'NewPassword',
        Justification = 'Generated or decrypted in memory moments earlier; sent to the server as a form field.')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingUsernameAndPasswordParams', '',
        Justification = 'The endpoint takes a login, the old password and the new one as form fields; there is no credential object to pass.')]
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    [OutputType([void])]
    param(
        [Parameter(Mandatory = $true)]
        [hashtable]$Connection,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Username,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$OldPassword,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$NewPassword
    )

    if (-not $PSCmdlet.ShouldProcess($Username, 'Change the password')) { return }

    $response = Send-FreeIPAHttpRequest -Connection $Connection -Path '/ipa/session/change_password' `
        -Form @{ user = $Username; old_password = $OldPassword; new_password = $NewPassword } -Accept 'text/plain'

    $result = 'error'
    if ($response.Headers.ContainsKey('X-IPA-Pwchange-Result')) { $result = [string]$response.Headers['X-IPA-Pwchange-Result'] }

    if ($result -eq 'ok') {
        Write-Verbose "Changed the password of $Username"
        return
    }

    $detail = ''
    if ($response.Headers.ContainsKey('X-IPA-Pwchange-Policy-Error')) { $detail = ': ' + [string]$response.Headers['X-IPA-Pwchange-Policy-Error'] }
    elseif (-not [string]::IsNullOrWhiteSpace($response.Body)) {
        $firstLine = @(($response.Body -split "`r?`n") | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -First 1)
        $detail = ': ' + ($firstLine -join '')
    }
    throw "FreeIPA refused the password change for $Username ($result)$detail"
}