Public/Get-GkAdminRoleAssignment.ps1

function Get-GkAdminRoleAssignment {
    <#
    .SYNOPSIS
        Report Entra directory role assignments — active, PIM-eligible, and PIM active/time-bound —
        with the assigned principal and role resolved.

    .DESCRIPTION
        Uses the modern role-management (RBAC) API rather than the legacy directoryRoles surface:
          * Active GET /roleManagement/directory/roleAssignments
          * Eligible (PIM) GET /roleManagement/directory/roleEligibilityScheduleInstances
          * Time-bound (PIM) GET /roleManagement/directory/roleAssignmentScheduleInstances
        Each is expanded with principal and roleDefinition. Rows are tagged with AssignmentKind
        (Active | Eligible | TimeBound) and include the directory scope (/ = tenant-wide).

        PIM (eligible/time-bound) data requires Microsoft Entra ID P2 / Governance. If those
        endpoints are unavailable (no P2) they are skipped with a warning and active assignments
        are still returned (degrade mode: warn and continue).

    .PARAMETER AssignmentKind
        Which assignment kinds to return: All (default), Active, Eligible, or TimeBound.

    .PARAMETER RoleName
        Client-side wildcard filter on the role display name (e.g. '*Admin*').

    .PARAMETER AsReport
        Add a ReportGeneratedUtc column for clean export.

    .EXAMPLE
        Get-GkAdminRoleAssignment

        All active, eligible, and time-bound role assignments in the tenant.

    .EXAMPLE
        Get-GkAdminRoleAssignment -AssignmentKind Eligible -RoleName '*Administrator*'

        PIM-eligible assignments to any *Administrator* role.

    .EXAMPLE
        Get-GkAdminRoleAssignment |
            Group-Object RoleName |
            Sort-Object Count -Descending |
            Select-Object Name, Count

        Count of assignments per role, most-assigned first.

    .OUTPUTS
        PSGraphKit.AdminRoleAssignment
    #>

    [CmdletBinding()]
    [OutputType('PSGraphKit.AdminRoleAssignment')]
    param(
        [ValidateSet('All', 'Active', 'Eligible', 'TimeBound')]
        [string] $AssignmentKind = 'All',

        [string] $RoleName,

        [switch] $AsReport
    )

    begin {
        Test-GkConnection -FunctionName 'Get-GkAdminRoleAssignment' | Out-Null
        $now = [datetime]::UtcNow
    }

    process {
        # Resolve role names via a one-time roleDefinitions lookup. (Graph rejects expanding both
        # principal and roleDefinition in one query — "only one property can be expanded" — so we
        # expand principal and map roleDefinitionId -> displayName ourselves.)
        # Key the map on BOTH id and templateId: an assignment's roleDefinitionId may reference
        # either, and for some built-in roles the two differ (an unresolved id would otherwise
        # surface as a raw GUID in RoleName).
        $roleMap = @{}
        foreach ($rd in (Invoke-GkGraphRequest -Uri '/roleManagement/directory/roleDefinitions?$select=id,displayName,templateId' -CallerFunction 'Get-GkAdminRoleAssignment')) {
            $rdName = [string](Get-GkDictValue $rd 'displayName')
            foreach ($key in @([string](Get-GkDictValue $rd 'id'), [string](Get-GkDictValue $rd 'templateId'))) {
                if ($key) { $roleMap[$key] = $rdName }
            }
        }

        $sources = @(
            @{ Kind = 'Active';    Uri = '/roleManagement/directory/roleAssignments?$expand=principal';                 Optional = $false }
            @{ Kind = 'Eligible';  Uri = '/roleManagement/directory/roleEligibilityScheduleInstances?$expand=principal'; Optional = $true }
            @{ Kind = 'TimeBound'; Uri = '/roleManagement/directory/roleAssignmentScheduleInstances?$expand=principal';  Optional = $true }
        )

        foreach ($source in $sources) {
            if ($AssignmentKind -ne 'All' -and $AssignmentKind -ne $source.Kind) { continue }

            try {
                $items = Invoke-GkGraphRequest -Uri $source.Uri -CallerFunction 'Get-GkAdminRoleAssignment'
            }
            catch {
                if ($source.Optional) {
                    Write-Warning "Could not read PIM $($source.Kind) assignments (requires Microsoft Entra ID P2 / Governance). Skipping. Underlying error: $($_.Exception.Message)"
                    continue
                }
                throw
            }

            foreach ($item in $items) {
                $principal = Get-GkDictValue $item 'principal'

                $odataType = [string](Get-GkDictValue $principal '@odata.type')
                $pType = switch ($odataType) {
                    '#microsoft.graph.user'             { 'User' }
                    '#microsoft.graph.group'            { 'Group' }
                    '#microsoft.graph.servicePrincipal' { 'ServicePrincipal' }
                    default { if ($odataType) { ($odataType -split '\.')[-1] } else { 'Unknown' } }
                }

                $roleDefId = [string](Get-GkDictValue $item 'roleDefinitionId')
                if (-not $roleMap.ContainsKey($roleDefId)) {
                    # The bulk roleDefinitions list is not exhaustive for every assigned role
                    # (e.g. some first-party/service-principal assignments). Resolve this one by id
                    # and cache the result (or the id itself if it cannot be resolved at all).
                    $resolved = $roleDefId
                    try {
                        $one = Invoke-GkGraphRequest -Raw -CallerFunction 'Get-GkAdminRoleAssignment' `
                            -Uri "/roleManagement/directory/roleDefinitions/$roleDefId`?`$select=id,displayName"
                        $nm = [string](Get-GkDictValue $one 'displayName')
                        if ($nm) { $resolved = $nm }
                    }
                    catch {
                        Write-Verbose "PSGraphKit: could not resolve roleDefinition $roleDefId : $($_.Exception.Message)"
                    }
                    $roleMap[$roleDefId] = $resolved
                }
                $roleDisplayName = $roleMap[$roleDefId]
                if ($RoleName -and $roleDisplayName -notlike $RoleName) { continue }

                $scope = [string](Get-GkDictValue $item 'directoryScopeId')

                $obj = [ordered]@{
                    PSTypeName        = 'PSGraphKit.AdminRoleAssignment'
                    RoleName          = $roleDisplayName
                    AssignmentKind    = $source.Kind
                    PrincipalName     = [string](Get-GkDictValue $principal 'displayName')
                    PrincipalUpn      = [string](Get-GkDictValue $principal 'userPrincipalName')
                    PrincipalType     = $pType
                    Scope             = $scope
                    IsTenantScope     = ($scope -eq '/')
                    AssignmentType    = [string](Get-GkDictValue $item 'assignmentType')
                    MemberType        = [string](Get-GkDictValue $item 'memberType')
                    StartDateTime     = ConvertTo-GkDateTime (Get-GkDictValue $item 'startDateTime')
                    EndDateTime       = ConvertTo-GkDateTime (Get-GkDictValue $item 'endDateTime')
                    RoleDefinitionId  = [string](Get-GkDictValue $item 'roleDefinitionId')
                    PrincipalId       = [string](Get-GkDictValue $item 'principalId')
                    AssignmentId      = [string](Get-GkDictValue $item 'id')
                }
                if ($AsReport) { $obj['ReportGeneratedUtc'] = $now }
                [pscustomobject]$obj
            }
        }
    }
}