Public/Get-InforcerGroup.ps1

function Get-InforcerGroup {
    <#
    .SYNOPSIS
        Retrieves groups from an Inforcer tenant.

        Required API scope(s): Tenants.Groups.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 groups or a single group from the Inforcer API.
        When called without -Group, returns all groups (GroupSummary objects) with optional filtering and auto-pagination.
        When called with -Group, resolves the group by GUID or display name and returns the full group detail (Group object) including members.

        Works with both login types. With client credentials the members of a single group come from a separate
        paged members call and are attached as the Members property, so the output matches an API-key login.
        That call reads every member page (100 members per page), so a very large group takes one call per 100 members.

        Use -Search for server-side prefix filtering (fast, sent to API).
        Use -Filter for client-side wildcard filtering (supports contains, e.g. *comp*).

        The list shows MembershipRule for dynamic groups. -Group additionally shows
        OnPremisesSyncEnabled.

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

    .PARAMETER Group
        The group to retrieve full details for. Accepts a GUID or a display name.
        When a GUID is provided, fetches the group directly.
        When a display name is provided, searches for the group and returns the detail including members.

    .PARAMETER Search
        Server-side search filter (sent to API). The API performs prefix matching on display name.
        For contains/substring matching, use -Filter instead.

    .PARAMETER Filter
        Client-side wildcard filter applied to display name after fetching all groups.
        Supports PowerShell wildcards: *comp* matches "All Company", empl* matches "All Employees".
        Fetches all groups from the API, then filters locally. Slower than -Search for large tenants
        but supports contains matching that the API does not.

    .PARAMETER MaxResults
        Maximum number of groups to return. 0 (default) means no limit. Only available when -Group is not specified.

    .PARAMETER OutputType
        Output type: 'PowerShellObject' (default) or 'JsonObject'.
        Piped tenants: each group carries TenantId and JsonObject returns one array for all tenants.

    .EXAMPLE
        Get-InforcerGroup -TenantId 'Contoso'

        Lists all groups in the tenant Contoso.

    .EXAMPLE
        Get-InforcerGroup -TenantId 'Contoso' -Search "Finance"

        Server-side search for groups starting with "Finance".

    .EXAMPLE
        Get-InforcerGroup -TenantId 'Contoso' -Filter "*comp*"

        Client-side filter for groups containing "comp" in the display name (e.g. "All Company").

    .EXAMPLE
        Get-InforcerGroup -TenantId 'Contoso' -Group "Tailspin Toys"

        Resolves the group by display name and returns full detail including members.

    .EXAMPLE
        Get-InforcerGroup -TenantId 'Contoso' -Group "00000000-0000-0000-0000-000000000006"

        Gets full detail for a specific group by GUID including members.

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

        Lists all groups in the piped tenant.

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

        Returns all groups as a JSON string.

    .EXAMPLE
        Get-InforcerGroup -TenantId 'Contoso' -Group "Finance" -OutputType JsonObject

        Returns full group detail as a JSON string.

    .OUTPUTS
        PSObject or String (when -OutputType JsonObject)

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

    .LINK
        Connect-Inforcer
    #>

    [CmdletBinding(DefaultParameterSetName = 'List')]
    [OutputType([PSObject], [string])]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'List')]
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'ById')]
        [Alias('ClientTenantId', 'MsTenantId')]
        [object]$TenantId,

        [Parameter(Mandatory, ParameterSetName = 'ById')]
        [Alias('GroupId')]
        [string]$Group,

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

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

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

        [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: resolve group (GUID or name) and get detail ---
            try {
                $resolvedGroupId = Resolve-InforcerGroupId -GroupId $Group -TenantId $resolvedTenantId
            } catch {
                Write-Error -Message $_.Exception.Message -ErrorId 'InvalidGroupId' -Category InvalidArgument
                return
            }

            $endpoint = "/tenants/$resolvedTenantId/groups/$resolvedGroupId"
            $err = $null
            $response = Invoke-InforcerApiRequest -Endpoint $endpoint -Method GET -OutputType PowerShellObject -PreserveStructure -ObjectType Group -ErrorVariable err -ErrorAction SilentlyContinue

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

            if ($script:InforcerSession.ApiVersion -eq 'v1') {
                $memberErr = $null
                $members = @(Invoke-InforcerPagedRequest -Endpoint "$endpoint/members" -ObjectType GroupMember -ErrorVariable memberErr -ErrorAction SilentlyContinue)
                if ($memberErr) {
                    Write-Warning "Could not read the members of group '$Group', so members is null: $($memberErr[-1].Exception.Message)"
                    $members = $null
                }
                Add-Member -InputObject $response -NotePropertyName 'members' -NotePropertyValue $members -Force
            }

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

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

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

        # Streams; -MaxResults counts matches, so with -Filter it applies after filtering.
        $listGroups = {
            if (-not $Filter) { return Invoke-InforcerPagedRequest -Endpoint $endpoint -ObjectType Group -MaxResults $MaxResults -ErrorVariable listErr }
            $first = if ($MaxResults -gt 0) { $MaxResults } else { [int]::MaxValue }
            Invoke-InforcerPagedRequest -Endpoint $endpoint -ObjectType Group -ErrorVariable listErr | Where-Object {
                $dn = $_.PSObject.Properties['displayName'].Value
                $dn -and $dn -like $Filter
            } | Select-Object -First $first
        }

        if ($OutputType -eq 'JsonObject') {
            $listErr = @()
            $groups = @(. $listGroups)
            if ($listErr) { return }
            if ($piped) { foreach ($g in $groups) { & $addTenantId $g; $pipedJson.Add($g) }; return }
            if ($script:InforcerSession.ApiVersion -eq 'v1') { return ConvertTo-Json -InputObject $groups -Depth 100 }
            return $groups | ConvertTo-Json -Depth 100
        }

        & $listGroups | ForEach-Object {
            & $addTenantId $_
            $null = Add-InforcerPropertyAliases -InputObject $_ -ObjectType GroupSummary
            $_.PSObject.TypeNames.Insert(0, 'InforcerCommunity.GroupSummary')
            $_
        }
    }

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