Providers/FreeIPA/Private/Invoke-FreeIPARequest.ps1
|
function Invoke-FreeIPARequest { <# .SYNOPSIS Calls one FreeIPA API method over JSON-RPC, with session renewal and a readable error .DESCRIPTION The single path every FreeIPA call takes. FreeIPA's API is JSON-RPC at /ipa/session/json: one method per call, named as the CLI names it with an underscore (user_add, group_add_member), positional arguments as a list and options as an object, and the whole thing behind a session cookie. That fixes what this function has to know: - The API version rides in the options. The one the server reported at connect time is sent, so the server never has to guess forward compatibility. - A call always answers HTTP 200, and success or failure is in the body: 'result' on success, or an 'error' object with a numeric code, a name such as NotFound or DuplicateEntry, and a message. Callers that expect a particular failure - a show that may find nothing, a modify that may change nothing - name it in -IgnoreError and get $null back; anything else is thrown with the method, the name and the server's message. - The session expires on idle. A 401 from the endpoint means it has, so the session is opened again with the connection's credential and the call is repeated once. - A search is unbounded here. The server applies a size limit and a two-second time limit by default, and a listing of three hundred users would be silently truncated without sizelimit and timelimit set to zero, so -Find sets both. Dates come back as {"__datetime__": "20261210024734Z"} objects and go out as that string; ConvertFrom-FreeIPADateTime and ConvertTo-FreeIPADateTime handle both ends. .PARAMETER Method The API method, as the CLI names it with underscores. .PARAMETER Arguments Positional arguments: the primary key for most methods, a search string for a find. .PARAMETER Options Named options, as the CLI's flags with underscores and without the leading dashes. .PARAMETER Find Mark the call as a search: sizelimit and timelimit are set to zero unless the caller set them, and the array of results is returned rather than the envelope. .PARAMETER NoLimit With -Find: send no size or time limit at all. automember_find is the one search that refuses the options every other search accepts, answering OptionError rather than ignoring them. .PARAMETER IgnoreError Error names that are an expected outcome. The call returns $null when one of them comes back instead of throwing. .PARAMETER Connection A connection to use instead of the active one. Tests pass one; the bootstrap passes the one it is in the middle of proving. .OUTPUTS System.Object. The 'result' envelope FreeIPA returned, whose 'result' property is the entry; or with -Find, the array of entries; or $null for an ignored error. .EXAMPLE PS> Invoke-FreeIPARequest -Method 'user_find' -Options @{ userclass = 'ZZ-TEST-seed' } -Find DESCRIPTION: Lists every user carrying the seed tag OUTPUT: An array of user entries USE CASE: Ownership discovery before teardown .EXAMPLE PS> Invoke-FreeIPARequest -Method 'group_add' -Arguments 'zz-test-all-staff' -Options @{ description = 'Every employee [ZZ-TEST-seed]' } DESCRIPTION: Creates a group OUTPUT: The envelope, with the group under .result USE CASE: Every create in the seed .NOTES Author: Jeffrey Stuhr Blog: https://www.techbyjeff.net LinkedIn: https://www.linkedin.com/in/jeffrey-stuhr-034214aa/ #> [CmdletBinding()] [OutputType([object])] param( [Parameter(Mandatory = $true)] [ValidatePattern('^[a-z][a-z0-9_]*$')] [string]$Method, [Parameter()] [object[]]$Arguments = @(), [Parameter()] [hashtable]$Options, [Parameter()] [switch]$Find, [Parameter()] [switch]$NoLimit, [Parameter()] [string[]]$IgnoreError = @(), [Parameter()] [hashtable]$Connection ) if (-not $Connection) { $Connection = Get-FreeIPAConnection } $requestOptions = @{} if ($Options) { foreach ($key in $Options.Keys) { if ($null -eq $Options[$key]) { continue } $requestOptions[[string]$key] = $Options[$key] } } if ($Connection.ApiVersion -and -not $requestOptions.ContainsKey('version')) { $requestOptions['version'] = [string]$Connection.ApiVersion } if ($Find -and -not $NoLimit) { if (-not $requestOptions.ContainsKey('sizelimit')) { $requestOptions['sizelimit'] = 0 } if (-not $requestOptions.ContainsKey('timelimit')) { $requestOptions['timelimit'] = 0 } } # The params element is always [arguments, options], and the arguments must serialise as # a JSON array even when there is one, which is what the unary comma protects. $payload = [ordered]@{ method = $Method params = @(, [object[]]@($Arguments)) + @(, $requestOptions) id = 0 } $json = $payload | ConvertTo-Json -Depth 20 -Compress $attempt = 0 while ($true) { $attempt++ $response = Send-FreeIPAHttpRequest -Connection $Connection -Path '/ipa/session/json' -Json $json -Accept 'application/json' if ($response.StatusCode -eq 401) { if ($attempt -ge 2) { throw "FreeIPA $Method failed: the session could not be renewed at $($Connection.BaseUrl)." } Write-Verbose 'The FreeIPA session has expired; opening a new one.' $login = Connect-FreeIPASession -Connection $Connection if (-not $login.Success) { throw "FreeIPA $Method failed: the session expired and the credential was rejected ($($login.Reason))." } continue } if ($response.StatusCode -ne 200) { $firstLine = @(($response.Body -split "`r?`n") | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -First 1) throw "FreeIPA $Method failed with HTTP $($response.StatusCode): $($firstLine -join '')" } $parsed = $null try { $parsed = $response.Body | ConvertFrom-Json -ErrorAction Stop } catch { throw "FreeIPA $Method answered something that is not JSON: $($_.Exception.Message)" } if ($parsed.PSObject.Properties['error'] -and $parsed.error) { $errorName = [string]$parsed.error.name if ($IgnoreError -contains $errorName) { Write-Verbose "FreeIPA $Method answered $errorName, which the caller expected." return $null } throw "FreeIPA $Method failed ($errorName $($parsed.error.code)): $($parsed.error.message)" } $envelope = $parsed.result if ($Find) { $entries = @() if ($envelope -and $envelope.PSObject.Properties['result']) { $entries = @($envelope.result) } if ($envelope -and $envelope.PSObject.Properties['truncated'] -and $envelope.truncated) { Write-Warning "FreeIPA truncated the $Method listing at $($entries.Count) entries; the server's own limit applied." } # Returned bare, deliberately: an empty array unrolls to nothing on the pipeline, # and every caller wraps the call in @(). return $entries } return $envelope } } |