public/Get-MtUser.ps1

function Get-MtUser {
    <#
    .SYNOPSIS
    Get a list of users from the tenant

    .DESCRIPTION
    This function retrieves a list of users from the tenant.
    You can specify the number of users to retrieve, the type of users (Member, Guest, Admin) and the role the users are member of.

    .PARAMETER Count
    The number of users to retrieve. Default is 1.

    .PARAMETER UserType
    The type of users to retrieve. Default is Member. Valid values are Member, Guest, Admin.

    .PARAMETER MemberOfRole
    The role the users are member of. Default is None. Valid values are Global administrator, Application administrator, Authentication Administrator, Billing administrator, Cloud application administrator, Conditional Access administrator, Exchange administrator, Helpdesk administrator, Password administrator, Privileged authentication administrator, Privileged Role Administrator, Security administrator, SharePoint administrator, User administrator.

    .EXAMPLE
    Get-MtUser -Count 5 -UserType Member
    # Get 5 Member users from the tenant.

    .LINK
    https://maester.dev/docs/commands/Get-MtUser
    #>

    [OutputType([System.Collections.ArrayList])]
    [CmdletBinding()]
    param (
        [Parameter()]
        [int]$Count = 1,

        [Parameter()]
        [ValidateSet("Member", "Guest", "Admin", "EmergencyAccess", "BreakGlass")]
        [string]$UserType = "Member",

        [Parameter()]
        [ValidateSet("Global administrator", "Application administrator", "Authentication Administrator", "Billing administrator", "Cloud application administrator", "Conditional Access administrator", "Exchange administrator", "Helpdesk administrator", "Password administrator", "Privileged authentication administrator", "Privileged Role Administrator", "Security administrator", "SharePoint administrator", "User administrator")]
        [string]$MemberOfRole
    )

    begin {

        $Users = New-Object -TypeName 'System.Collections.ArrayList'

        # Default roles that will be queried for UserType "Admin"
        $AdminRoles = @(
            "Global administrator",
            "Application administrator",
            "Authentication Administrator",
            "Billing administrator",
            "Cloud application administrator",
            "Conditional Access administrator",
            "Exchange administrator",
            "Helpdesk administrator",
            "Password administrator",
            "Privileged authentication administrator",
            "Privileged Role Administrator",
            "Security administrator",
            "SharePoint administrator",
            "User administrator"
        )
    }

    process {

        Write-Verbose "Getting $Count $UserType users from the tenant."

        if ( $UserType -eq "Admin" ) {
            $UserType = "Member"
            if ( $MemberOfRole ) {
                Write-Verbose "Getting $UserType users that are member of $MemberOfRole."
                $AdminRoles = $MemberOfRole
            } else {
                Write-Verbose "Getting $UserType users that are member of any admin role."
            }
            $EntraIDRoles = Invoke-MtGraphRequest -ApiVersion beta 'directoryRoles' | Where-Object { $_.displayName -in $AdminRoles } | Select-Object id, displayName
            foreach ( $EntraIDRole in $EntraIDRoles ) {
                $TmpUsers = Invoke-MtGraphRequest -RelativeUri "directoryRoles/$($EntraIDRole.id)/members" -Select id, userPrincipalName, userType -OutputType Hashtable
                if ( $TmpUsers.ContainsKey('userType') ) {
                    Write-Verbose "Setting userType to Admin for $(($TmpUsers | Measure-Object).count) users that are member of $($EntraIDRole.displayName)."
                    $TmpUsers | ForEach-Object {
                        $_.userType = "Admin"
                        $Users.Add($_) | Out-Null
                        if ($Users.Count -ge $Count) {
                            Write-Verbose "Found $Count $UserType users."
                            break
                        }
                    }
                }
            }
        } elseif ( $UserType -in @("BreakGlass", "EmergencyAccess") ) {
            Write-Verbose "Getting $UserType users from the tenant."
            Write-Verbose "Get all conditional access policies."
            # Get all policies (the state of policy does not have to be enabled)
            $CAPolicies = Get-MtConditionalAccessPolicy | Where-Object { -not $_.conditions.applications.includeAuthenticationContextClassReferences }

            # Check which user object Id or group object Id is excluded from the most policies
            $PossibleEmergencyAccessUsers = $CAPolicies.conditions.users.excludeUsers | Group-Object -NoElement | Sort-Object -Property Count -Descending | Select-Object -First 2
            if ($PossibleEmergencyAccessUsers.Count -eq 2) {
                # Check if the number of excluded policies is the same for all possible users
                $EmergencyAccessUsers = $PossibleEmergencyAccessUsers | Group-Object -Property Count | Sort-Object -Property Name -Descending | Select-Object -First 1 -ExpandProperty Group
                $EmergencyAccessUsers = $EmergencyAccessUsers | Select-Object -ExpandProperty Name -Unique
            }
            $PossibleEmergencyAccessGroups = $CAPolicies.conditions.users.excludeGroups | Group-Object -NoElement | Sort-Object -Property Count -Descending | Select-Object -First 2
            if ($PossibleEmergencyAccessGroups.Count -eq 2) {
                # Check if the number of excluded policies is the same for all possible users
                $EmergencyAccessGroups = $PossibleEmergencyAccessGroups | Group-Object -Property Count | Sort-Object -Property Name -Descending | Select-Object -First 1 -ExpandProperty Group
                $EmergencyAccessGroups = $EmergencyAccessGroups | Select-Object -ExpandProperty Name -Unique
            }
            # If the number of excluded users is higher than the number of excluded groups, check the user object GUID
            $EmergencyAccessUsersCount = $CApolicies.conditions.users.excludeUsers | Where-Object { $_ -in $EmergencyAccessUsers } | Measure-Object | Select-Object -ExpandProperty Count
            $EmergencyAccessGroupsCount = $CApolicies.conditions.users.excludeGroups | Where-Object { $_ -in $EmergencyAccessGroups } | Measure-Object | Select-Object -ExpandProperty Count
            if ( $EmergencyAccessUsersCount -gt $EmergencyAccessGroupsCount ) {
                # Handling Emergency Access Users
                foreach ( $EmergencyAccessUser in $EmergencyAccessUsers ) {
                    try {
                        $TmpUsers = Invoke-MtGraphRequest -RelativeUri "users/$EmergencyAccessUser" -Select id, userPrincipalName, userType -OutputType Hashtable
                        if ( $TmpUsers.ContainsKey('userType') ) {
                            Write-Verbose "Setting userType to $UserType for $(($TmpUsers | Measure-Object).count) users that are member of EmergencyAccess."
                            $TmpUsers | ForEach-Object {
                                $_.userType = "EmergencyAccess"
                                $Users.Add($_) | Out-Null

                                if ($Users.Count -ge $Count) {
                                    Write-Verbose "Found $Count $UserType users."
                                    break
                                }
                            }
                        }
                    } catch {
                        Write-Warning -Message "Unable to retrieve user with GUID: ${EmergencyAccessUser}"
                    }
                }
            } else {
                # Handling Emergency Access Groups
                Write-Verbose "Emergency access group: $EmergencyAccessGroups"
                foreach ( $EmergencyAccessGroup in $EmergencyAccessGroups ) {
                    # Skip null or empty group IDs that can occur when CA policies have no group exclusions
                    if ([string]::IsNullOrEmpty($EmergencyAccessGroup)) {
                        Write-Verbose "Skipping null or empty emergency access group ID."
                        continue
                    }
                    # Fetch only the first page to avoid timeout on large groups. Fix for https://github.com/maester365/maester/issues/1227
                    # -DisablePaging causes Invoke-MtGraphRequest to return the raw Graph response wrapper
                    # ({ value: [...], @odata.context: '...', ... }) rather than the unwrapped member objects.
                    # Extract the members array from the value property before counting or iterating.
                    try {
                        $RawResponse = Invoke-MtGraphRequest -RelativeUri "groups/$EmergencyAccessGroup/members" -Select id, userPrincipalName, userType -OutputType Hashtable -DisablePaging
                        $TmpUsers = if ($null -ne $RawResponse -and $RawResponse.ContainsKey('value')) { @($RawResponse['value']) } else { @() }
                        # Reject groups that are too large to be emergency access groups. If the first page already has
                        # many members it is likely a broad corporate group rather than an emergency access group.
                        # See https://github.com/maester365/maester/issues/1227
                        $MemberCount = $TmpUsers.Count
                        if ($MemberCount -gt 20) {
                            Write-Warning "Get-MtUser: Skipping group '$EmergencyAccessGroup' — it has $MemberCount members, which is too many to be an emergency access group. Emergency access groups should have only 1–2 members. Review your Conditional Access policy exclusions to confirm the correct group is being excluded."
                            continue
                        }
                        Write-Verbose "Setting userType to EmergencyAccess for $MemberCount users that are members of group '$EmergencyAccessGroup'."
                        $TmpUsers | ForEach-Object {
                            $_.userType = "EmergencyAccess"
                            $Users.Add($_) | Out-Null

                            if ($Users.Count -ge $Count) {
                                Write-Verbose "Found $Count $UserType users."
                                break
                            }
                        }
                    } catch {
                        Write-Warning -Message "Unable to retrieve members for group with GUID: ${EmergencyAccessGroup}. Error: $_"
                    }
                }
            }
        } else {
            if ( $UserType -eq "Member" ) {
                $queryFilter = "userType eq 'Member'"
            } elseif ( $UserType -eq "Guest" ) {
                $queryFilter = "userType eq 'Guest'"
            } else {
                Write-Warning "UserType $($UserType) cannot be processed! Aborting!"
                throw "User can not be queried, invalid UserType: $($UserType)"
            }

            if ($Count -gt 999) {
                Write-Verbose "The maximum number of users that can be retrieved on one page is 999. Using paging to retrieve $Count users."

                $TmpUsers = Invoke-MtGraphRequest -ApiVersion beta -RelativeUri 'users' -Select id, userPrincipalName, userType -Filter $queryFilter -QueryParameters @{'$top' = 999 } -OutputType Hashtable
                $Count = if ( $TmpUsers.Count -lt $Count ) { $TmpUsers.Count } else { $Count }

                Write-Verbose "Retrieved $($TmpUsers.Count) users"
                for ($i = 0; $i -lt $Count; $i++) {
                    $Users.Add($TmpUsers[$i]) | Out-Null
                }
            } else {
                $Users = Invoke-MtGraphRequest -ApiVersion beta -RelativeUri 'users' -Select id, userPrincipalName, userType -Filter $queryFilter -QueryParameters @{'$top' = $Count } -DisablePaging -OutputType Hashtable
                $Users = $Users['value']
            }
        }

        return $Users
    }
}