Public/Get-InforcerUser.ps1

function Get-InforcerUser {
    <#
    .SYNOPSIS
        Retrieves users from an Inforcer tenant.

        Required API scope(s): Tenants.Users.Read + Tenants.Read (only when -TenantId is a GUID or tenant name)
        (API key); User & Group Management + Tenants (only for a tenant name) permission (API client, unconfirmed).

    .DESCRIPTION
        Gets a list of users or a single user by ID from the Inforcer API. Works with both login types (API key and client credentials).
        When called without -UserId, returns all users (UserSummary objects) with optional search filtering and auto-pagination.
        When called with -UserId, returns the full user detail (User object) including groups, roles, devices, and risk information.

    .PARAMETER Format
        Output format. Currently only 'Raw' is supported.

    .PARAMETER TenantId
        The Inforcer tenant ID. Accepts numeric ID (API-key login only), Microsoft tenant GUID, or tenant name. Supports pipeline input.

    .PARAMETER Search
        Server-side search filter for the user list. Only available in the List parameter set.

    .PARAMETER MaxResults
        Maximum number of users to return. 0 (default) means no limit. Only available in the List parameter set.

    .PARAMETER UserId
        The user ID (GUID) to retrieve full details for. Only available in the ById parameter set.

    .PARAMETER OutputType
        Output type: 'PowerShellObject' (default) or 'JsonObject'. Depth 100; the v1 list is always a JSON array.
        Piped tenants: each user carries TenantId and JsonObject returns one array for all tenants.

    .EXAMPLE
        Get-InforcerUser -TenantId 'Contoso'

        Lists all users in the tenant Contoso.

    .EXAMPLE
        Get-InforcerUser -TenantId 'Contoso' -Search "Adele"

        Searches for users matching "Adele" in the tenant Contoso.

    .EXAMPLE
        Get-InforcerUser -TenantId 'Contoso' -UserId "00000000-0000-0000-0000-000000000005"

        Gets full detail for a specific user.

    .EXAMPLE
        Get-InforcerTenant -TenantId 'Contoso' | Get-InforcerUser

        Lists all users in the piped tenant.

    .EXAMPLE
        Get-InforcerUser -TenantId 'Contoso' -OutputType JsonObject

        Returns the users as a JSON string (depth 100) instead of objects.

    .OUTPUTS
        PSObject or String (when -OutputType JsonObject)

    .LINK
        https://github.com/royklo/InforcerCommunity/blob/main/docs/CMDLET-REFERENCE.md#get-inforceruser

    .LINK
        Connect-Inforcer
    #>

    [CmdletBinding(DefaultParameterSetName = 'List')]
    [OutputType([PSObject], [string])]
    param(
        [Parameter(ParameterSetName = 'List')]
        [Parameter(ParameterSetName = 'ById')]
        [ValidateSet('Raw')]
        [string]$Format = 'Raw',

        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'List')]
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'ById')]
        [Alias('ClientTenantId', 'MsTenantId')]
        [object]$TenantId,

        [Parameter(ParameterSetName = 'List')]
        [string]$Search,

        [Parameter(ParameterSetName = 'List')]
        [int]$MaxResults = 0,

        [Parameter(Mandatory, ParameterSetName = 'ById')]
        [string]$UserId,

        [Parameter(ParameterSetName = 'List')]
        [Parameter(ParameterSetName = 'ById')]
        [ValidateSet('PowerShellObject', 'JsonObject')]
        [string]$OutputType = 'PowerShellObject'
    )

    begin { $pipedJson = [System.Collections.Generic.List[object]]::new() }

    process {
        if (-not (Test-InforcerSession)) {
            Write-Error -Message "Not connected to Inforcer. Use Connect-Inforcer first." -ErrorId 'NotConnected' -Category ConnectionError
            return
        }

        try {
            $resolvedTenantId = Resolve-InforcerTenantId -TenantId $TenantId
        } catch {
            Write-Error -Message $_.Exception.Message -ErrorId 'InvalidTenantId' -Category InvalidArgument
            return
        }

        $piped = $PSCmdlet.MyInvocation.ExpectingInput
        $addTenantId = {
            param($item)
            if ($piped -and $item -is [PSObject] -and -not $item.PSObject.Properties['TenantId']) {
                $item.PSObject.Properties.Add([System.Management.Automation.PSNoteProperty]::new('TenantId', $resolvedTenantId))
            }
        }

        if ($PSCmdlet.ParameterSetName -eq 'ById') {
            # --- ById: single user detail ---
            $endpoint = "/tenants/$resolvedTenantId/users/$UserId"
            $err = $null
            # -PreserveStructure: user detail response is a single object with many nested arrays
            # (groups, roles, devices, appRoleAssignments, assignedLicenses, businessPhones, ...).
            # Without it, Invoke-InforcerApiRequest would unwrap to the first array property.
            $response = Invoke-InforcerApiRequest -Endpoint $endpoint -Method GET -OutputType PowerShellObject -PreserveStructure -ObjectType User -ErrorVariable err -ErrorAction SilentlyContinue

            if ($null -eq $response) {
                if (-not $err -or $err[-1].FullyQualifiedErrorId -like 'ApiRequestFailed_404*') {
                    Write-Error -Message "User '$UserId' not found in tenant '$resolvedTenantId'." -ErrorId 'UserNotFound' -Category ObjectNotFound
                } else {
                    Write-Error -ErrorRecord $err[-1]
                }
                return
            }

            & $addTenantId $response
            if ($OutputType -eq 'JsonObject') {
                if ($piped) { $pipedJson.Add($response); return }
                return $response | ConvertTo-Json -Depth 100
            }

            $null = Add-InforcerPropertyAliases -InputObject $response -ObjectType User
            $response.PSObject.TypeNames.Insert(0, 'InforcerCommunity.User')
            return $response
        }

        # --- List: paginated user list ---
        $endpoint = "/tenants/$resolvedTenantId/users"
        if ($Search) { $endpoint += '?search=' + [System.Uri]::EscapeDataString($Search) }

        if ($OutputType -eq 'JsonObject') {
            $users = @(Invoke-InforcerPagedRequest -Endpoint $endpoint -ObjectType User -MaxResults $MaxResults -ErrorVariable listErr)
            if ($listErr) { return }
            if ($piped) { foreach ($u in $users) { & $addTenantId $u; $pipedJson.Add($u) }; return }
            if ($script:InforcerSession.ApiVersion -eq 'v1') { return ConvertTo-Json -InputObject $users -Depth 100 }
            return $users | ConvertTo-Json -Depth 100
        }

        Invoke-InforcerPagedRequest -Endpoint $endpoint -ObjectType User -MaxResults $MaxResults | ForEach-Object {
            & $addTenantId $_
            $null = Add-InforcerPropertyAliases -InputObject $_ -ObjectType UserSummary
            $_.PSObject.TypeNames.Insert(0, 'InforcerCommunity.UserSummary')
            $_
        }
    }

    end {
        if ($PSCmdlet.MyInvocation.ExpectingInput -and $OutputType -eq 'JsonObject' -and (Test-InforcerSession)) {
            ConvertTo-Json -InputObject @($pipedJson) -Depth 100
        }
    }
}