functions/user/Get-PSEntraIDUserGuest.ps1

function Get-PSEntraIDUserGuest {
    <#
    .SYNOPSIS
        Retrieves properties of users in Entra ID (Azure AD), but only Guest accounts.

    .DESCRIPTION
        Cmdlet for retrieving users with "userType eq 'Guest'".
        Supports multiple parameter sets (Identity, Name, CompanyName, Filter, All)
        and always returns only Guest accounts.

    .PARAMETER Identity
        UserPrincipalName, Mail, or Id of the user in the tenant.
        If the user exists but is not a Guest, no output is returned.

    .PARAMETER Name
        DisplayName, GivenName, or SurName of the user in the tenant.

    .PARAMETER CompanyName
        CompanyName of the user in the tenant.

    .PARAMETER Disabled
        Returns only disabled accounts (accountEnabled eq false).

    .PARAMETER Filter
        Custom OData filter expression for filtering users, combined with "userType eq 'Guest'".

    .PARAMETER AdvancedFilter
        Enables the use of the ConsistencyLevel = 'eventual' header (e.g., for $count).

    .PARAMETER All
        Returns all users in the tenant, but only those with "userType eq 'Guest'".

    .PARAMETER Issuer
        Matches guests whose identities collection carries this issuer - the home
        organization's domain for a B2B guest ("contoso.com"), or the provider for a
        social one ("google.com", "facebook.com"), or "mail" for an email one-time
        passcode guest.

        Graph matches an issuer on its own only for google.com, facebook.com, mail and
        phone. For any other issuer supply -IssuerAssignedId as well; without it the
        request comes back empty rather than failing, and the cmdlet warns to say so.

    .PARAMETER IssuerAssignedId
        The identifier the issuer assigned to the guest, normally their sign-in name at
        the home organization. Combined with -Issuer.

    .PARAMETER SignInType
        Narrows the identity match to one sign-in type, such as federated, userName or
        emailAddress. userPrincipalName is rejected: Graph documents it as unsupported
        for filtering, and returns an empty set instead of an error. Use -Identity to
        look an account up by its UPN.

    .PARAMETER EnableException
        Enables exception throwing instead of friendly warnings.

    .EXAMPLE
        PS C:\> Get-PSEntraIDUserGuest -Identity user1@contoso.com

        Returns details for user1@contoso.com, only if it is a Guest account.

    .EXAMPLE
        PS C:\> Get-PSEntraIDUserGuest -All

        Returns all Guest accounts in the tenant.

    .EXAMPLE
        PS C:\> Get-PSEntraIDUserGuest -Issuer 'contoso.com' -IssuerAssignedId 'j.smith@contoso.com'

        Returns the guest that signs in as j.smith@contoso.com at the Contoso tenant,
        whatever their user principal name in this tenant happens to be.

    .EXAMPLE
        PS C:\> Get-PSEntraIDUserGuest -Issuer 'google.com'

        Returns every guest signing in with a Google identity. One of the four issuers
        Graph will match without an issuerAssignedId.

    .EXAMPLE
        PS C:\> Get-PSEntraIDUserGuest -All | Group-Object HomeIssuer -NoElement | Sort-Object Count -Descending

        Which external organizations the tenant's guests actually come from. HomeIssuer
        is read off the identities collection, so it is the issuer the guest signs in
        with rather than a guess from the #EXT# user principal name.

        .NOTES
        Piping into Select-Object -First N logs a warning that is not a failure:

            WARNING: [<cmdlet>] Failed to: ... | The pipeline has been stopped

        The results are correct and complete. Select-Object stops the pipeline once it
        has what it asked for, and the next write throws PipelineStoppedException -
        normal termination, reported as an error only because the write happens inside a
        protected block. Materialise first if the warning is in the way:

            $items = @(<cmdlet> ...)
            $items | Select-Object -First 3

        or filter server-side with -Filter instead of trimming client-side. Not silenced
        on purpose: the only fix that works is to collect the whole result before
        emitting any of it, which would cost streaming on every read.

#>

    [OutputType('PSMicrosoftEntraID.Users.UserGuest')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'Parameters consumed inside Where-Object script blocks or reserved as part of the public parameter surface.')]
    [CmdletBinding(DefaultParameterSetName = 'Identity')]
    param (
        [Parameter(Mandatory = $True, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Identity')]
        [Alias("Id", "UserPrincipalName", "Mail")]
        [ValidateNotNullOrEmpty()]
        [string[]] $Identity,

        [Parameter(Mandatory = $True, ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false, ParameterSetName = 'Name')]
        [ValidateNotNullOrEmpty()]
        [string[]] $Name,

        [Parameter(Mandatory = $True, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'CompanyName')]
        [ValidateNotNullOrEmpty()]
        [string[]] $CompanyName,

        [Parameter(Mandatory = $false, ParameterSetName = 'CompanyName')]
        [Parameter(Mandatory = $false, ParameterSetName = 'All')]
        [switch] $Disabled,

        [Parameter(Mandatory = $True, ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false, ParameterSetName = 'Filter')]
        [ValidateNotNullOrEmpty()]
        [string] $Filter,

        [Parameter(Mandatory = $false, ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false, ParameterSetName = 'Filter')]
        [switch] $AdvancedFilter,

        [Parameter(Mandatory = $True, ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false, ParameterSetName = 'All')]
        [switch] $All,

        [Parameter(Mandatory = $True, ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false, ParameterSetName = 'Identities')]
        [ValidateNotNullOrEmpty()]
        [string] $Issuer,

        [Parameter(Mandatory = $false, ParameterSetName = 'Identities')]
        [ValidateNotNullOrEmpty()]
        [string] $IssuerAssignedId,

        [Parameter(Mandatory = $false, ParameterSetName = 'Identities')]
        [ValidateNotNullOrEmpty()]
        [string] $SignInType,

        [Parameter()]
        [switch] $EnableException
    )

    begin {
        [string] $service = Get-PSFConfigValue -FullName ('{0}.Settings.DefaultService' -f $script:ModuleName)
        Assert-EntraConnection -Service $service -Cmdlet $PSCmdlet

        [hashtable] $query = @{
            '$count'  = 'true'
            '$top'    = Get-PSFConfigValue -FullName ('{0}.Settings.GraphApiQuery.PageSize' -f $script:ModuleName)
            '$select' = ((Get-PSFConfig -Module $script:ModuleName -Name Settings.GraphApiQuery.Select.UserGuest).Value -join ',')
        }

        [int] $commandRetryCount = Get-PSFConfigValue -FullName ('{0}.Settings.Command.RetryCount' -f $script:ModuleName)
        [System.TimeSpan] $commandRetryWait = New-TimeSpan -Seconds (Get-PSFConfigValue -FullName ('{0}.Settings.Command.RetryWaitInSeconds' -f $script:ModuleName))
    }

    process {
        switch ($PSCmdlet.ParameterSetName) {
            'Identity' {
                foreach ($user in $Identity) {
                    [hashtable] $mailQuery = @{
                        #'$count' = 'true'
                        '$top'    = Get-PSFConfigValue -FullName ('{0}.Settings.GraphApiQuery.PageSize' -f $script:ModuleName)
                        '$select' = ((Get-PSFConfig -Module $script:ModuleName -Name Settings.GraphApiQuery.Select.User).Value -join ',')
                    }
                    $mailQuery['$Filter'] = ("mail eq '{0}'" -f (ConvertTo-ODataFilterString -Value $user))
                    Invoke-PSFProtectedCommand -ActionString 'User.Get' -ActionStringValues $user -Target (Get-PSFLocalizedString -Module $script:ModuleName -Name Identity.Platform) -ScriptBlock {
                        [PSMicrosoftEntraID.Users.UserGuest[]] $userMail = ConvertFrom-RestUserGuest -InputObject (
                            Invoke-EntraRequest -Service $service -Path 'users' -Query $mailQuery -Method Get -ErrorAction Stop
                        )

                        if (-not $userMail) {
                            $userId = $user
                        }
                        else {
                            $userId = $userMail[0].Id
                        }

                        # Escaped because a guest's UPN always contains '#EXT#', and an
                        # unescaped '#' starts the URI fragment - the path is silently
                        # truncated and Graph answers for a user that does not exist.
                        # See the same fix in Get-PSEntraIDUser.
                        $fullUser = ConvertFrom-RestUserGuest -InputObject (
                            Invoke-EntraRequest -Service $service -Path ("users/{0}" -f [uri]::EscapeDataString($userId)) -Query $query -Method Get -ErrorAction Stop
                        )

                        if ($fullUser.UserType -eq 'Guest') {
                            $fullUser
                        }
                    } -EnableException:$EnableException -PSCmdlet $PSCmdlet -Continue -RetryCount $commandRetryCount -RetryWait $commandRetryWait -WhatIf:$false
                    if (Test-PSFFunctionInterrupt) { return }
                }
            }
            'Filter' {
                $completeFilter = Add-GuestFilter $Filter
                $query['$Filter'] = $completeFilter

                if ($AdvancedFilter.IsPresent) {
                    $header = @{ 'ConsistencyLevel' = 'eventual' }
                    Invoke-PSFProtectedCommand -ActionString 'User.Filter' -ActionStringValues $completeFilter -Target (Get-PSFLocalizedString -Module $script:ModuleName -Name Identity.Platform) -ScriptBlock {
                        ConvertFrom-RestUserGuest -InputObject (
                            Invoke-EntraRequest -Service $service -Path 'users' -Query $query -Method Get -Header $header -ErrorAction Stop
                        )
                    } -EnableException:$EnableException -PSCmdlet $PSCmdlet -Continue -RetryCount $commandRetryCount -RetryWait $commandRetryWait -WhatIf:$false
                    if (Test-PSFFunctionInterrupt) { return }
                }
                else {
                    Invoke-PSFProtectedCommand -ActionString 'User.Filter' -ActionStringValues $completeFilter -Target (Get-PSFLocalizedString -Module $script:ModuleName -Name Identity.Platform) -ScriptBlock {
                        ConvertFrom-RestUserGuest -InputObject (
                            Invoke-EntraRequest -Service $service -Path 'users' -Query $query -Method Get -ErrorAction Stop
                        )
                    } -EnableException:$EnableException -PSCmdlet $PSCmdlet -Continue -RetryCount $commandRetryCount -RetryWait $commandRetryWait -WhatIf:$false
                    if (Test-PSFFunctionInterrupt) { return }
                }
            }
            'CompanyName' {
                [hashtable] $header = @{}
                $header['ConsistencyLevel'] = 'eventual'
                [string] $companyNameList = ($CompanyName | ForEach-Object { "'{0}'" -f (ConvertTo-ODataFilterString -Value $_) } | Join-String -Separator ',')
                if ($Disabled.IsPresent) {
                    $completeFilter = Add-GuestFilter ('companyName in ({0}) and accountEnabled eq false' -f $companyNameList)
                    $query['$Filter'] = $completeFilter
                    Invoke-PSFProtectedCommand -ActionString 'User.Filter' -ActionStringValues ('companyName in ({0}) and accountEnabled eq false' -f $companyNameList) -Target (Get-PSFLocalizedString -Module $script:ModuleName -Name Identity.Platform) -ScriptBlock {
                        ConvertFrom-RestUserGuest -InputObject (Invoke-EntraRequest -Service $service -Path ('users') -Header $header -Query $query -Method Get -ErrorAction Stop)
                    } -EnableException:$EnableException -PSCmdlet $PSCmdlet -Continue -RetryCount $commandRetryCount -RetryWait $commandRetryWait -WhatIf:$false
                    if (Test-PSFFunctionInterrupt) { return }
                }
                else {
                    $completeFilter = Add-GuestFilter ('companyName in ({0})' -f $companyNameList)
                    $query['$Filter'] = $completeFilter
                    Invoke-PSFProtectedCommand -ActionString 'User.Filter' -ActionStringValues ('companyName in ({0})' -f $companyNameList) -Target (Get-PSFLocalizedString -Module $script:ModuleName -Name Identity.Platform) -ScriptBlock {
                        ConvertFrom-RestUserGuest -InputObject (Invoke-EntraRequest -Service $service -Path ('users') -Header $header -Query $query -Method Get -ErrorAction Stop)
                    } -EnableException:$EnableException -PSCmdlet $PSCmdlet -Continue -RetryCount $commandRetryCount -RetryWait $commandRetryWait -WhatIf:$false
                    if (Test-PSFFunctionInterrupt) { return }
                }
            }
            'Identities' {
                $identityFilter = New-EntraIdentityFilter -Cmdlet $PSCmdlet -Issuer $Issuer -IssuerAssignedId $IssuerAssignedId -SignInType $SignInType
                $completeFilter = Add-GuestFilter $identityFilter
                $query['$Filter'] = $completeFilter

                # A lambda over a collection is an advanced query in Graph's terms, so the
                # eventual consistency header is not optional here the way -AdvancedFilter
                # is on the Filter set. Without it Graph rejects the request outright.
                [hashtable] $header = @{ 'ConsistencyLevel' = 'eventual' }

                Invoke-PSFProtectedCommand -ActionString 'User.Identity.Filter' -ActionStringValues $identityFilter -Target (Get-PSFLocalizedString -Module $script:ModuleName -Name Identity.Platform) -ScriptBlock {
                    ConvertFrom-RestUserGuest -InputObject (
                        Invoke-EntraRequest -Service $service -Path 'users' -Query $query -Method Get -Header $header -ErrorAction Stop
                    )
                } -EnableException:$EnableException -PSCmdlet $PSCmdlet -Continue -RetryCount $commandRetryCount -RetryWait $commandRetryWait -WhatIf:$false
                if (Test-PSFFunctionInterrupt) { return }
            }
            'All' {
                if ($All.IsPresent) {
                    if ($Disabled.IsPresent) {
                        [hashtable] $header = @{}
                        $header['ConsistencyLevel'] = 'eventual'
                        $completeFilter = Add-GuestFilter "accountEnabled eq false"
                        $query['$Filter'] = $completeFilter
                        Invoke-PSFProtectedCommand -ActionString 'User.Filter' -ActionStringValues 'accountEnabled eq false' -Target (Get-PSFLocalizedString -Module $script:ModuleName -Name Identity.Platform) -ScriptBlock {
                            ConvertFrom-RestUserGuest -InputObject (Invoke-EntraRequest -Service $service -Path ('users') -Header $header -Query $query -Method Get -ErrorAction Stop)
                        } -EnableException:$EnableException -PSCmdlet $PSCmdlet -Continue -RetryCount $commandRetryCount -RetryWait $commandRetryWait -WhatIf:$false
                        if (Test-PSFFunctionInterrupt) { return }
                    }
                    else {
                        $query['$Filter'] = "userType eq 'Guest'"
                        Invoke-PSFProtectedCommand -ActionString 'User.List' -ActionStringValues 'All' -Target (Get-PSFLocalizedString -Module $script:ModuleName -Name Identity.Platform) -ScriptBlock {
                            ConvertFrom-RestUserGuest -InputObject (Invoke-EntraRequest -Service $service -Path ('users') -Query $query -Method Get -ErrorAction Stop)
                        } -EnableException:$EnableException -PSCmdlet $PSCmdlet -Continue -RetryCount $commandRetryCount -RetryWait $commandRetryWait -WhatIf:$false
                        if (Test-PSFFunctionInterrupt) { return }
                    }
                }
            }
        }
    }
    end {}
}