Providers/FabricProbeProvider.ps1

function New-FabricProbeProvider {
    <#
    .SYNOPSIS
        Constructs the Microsoft Fabric RBAC probe provider.
    .DESCRIPTION
        Internal factory. Fabric has no action-based RBAC model and its REST
        errors (InsufficientPrivileges / Unauthorized) do not name a required
        role, so error-parsing cannot derive a requirement. Access is governed by
        the four workspace roles - Admin > Member > Contributor > Viewer - plus
        item permissions and tenant settings. This provider resolves requirements
        from the knowledge base and evaluates access by listing workspace role
        assignments via the Fabric REST API
        (GET /v1/workspaces/{id}/roleAssignments). The required role is satisfied
        by any role at or above it in the hierarchy.
    .OUTPUTS
        PSCustomObject (PSAutoRBAC.Provider)
    #>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Factory; constructs and returns a provider object, makes no state change.')]
    [OutputType([psobject])]
    param()

    $resourceUrl = 'https://api.fabric.microsoft.com'
    # Higher number = more privilege. Used so a higher role satisfies a lower requirement.
    $rank = @{ 'Viewer' = 1; 'Contributor' = 2; 'Member' = 3; 'Admin' = 4 }

    [pscustomobject]@{
        PSTypeName        = 'PSAutoRBAC.Provider'
        Name              = 'Microsoft Fabric'
        Aliases           = @('Fabric', 'PowerBI', 'Power BI')
        SupportsLiveProbe = $false
        MatchScope        = { param($Assignment, $Scope) $true }

        ResolveRequirement = {
            param($Command, $Context, $Options)
            Write-PSFMessage -Level Verbose -Message "Fabric: resolving requirement for '$Command'." -Tag 'PSAutoRBAC', 'Provider', 'Fabric'
            $mapPath = if ($Options -and $Options.ContainsKey('MapPath')) { $Options.MapPath } else { $null }
            $map = if ($mapPath) { Get-RBACKnowledgeBase -Path $mapPath } else { Get-RBACKnowledgeBase -Name 'CommandRoleMap' }
            $commands = $map['Microsoft Fabric']
            $commandKey = $commands.Keys | Where-Object { $_ -eq $Command } | Select-Object -First 1
            $isKnown = [bool]$commandKey
            if (-not $commandKey) { $commandKey = '*Default' }
            $entry = $commands[$commandKey]

            [pscustomobject]@{
                PSTypeName  = 'PSAutoRBAC.Requirement'
                Platform    = 'Microsoft Fabric'
                Command     = $Command
                Roles       = @($entry.Roles)
                Permissions = @($entry.Actions)
                ScopeLevel  = $entry.ScopeLevel
                Notes       = $entry.Notes
                IsKnown     = $isKnown
                Source      = 'KnowledgeBase'
            }
        }

        TestAccess = {
            param($CallerId, $RequiredRole, $Scope, $Context, $Options)

            Write-PSFMessage -Level Verbose -Message "Fabric: testing '$CallerId' for [$(@($RequiredRole) -join ', ')] on workspace '$Scope'." -Tag 'PSAutoRBAC', 'Provider', 'Fabric'
            $rank = @{ 'Viewer' = 1; 'Contributor' = 2; 'Member' = 3; 'Admin' = 4 }
            $resourceUrl = 'https://api.fabric.microsoft.com'

            # Determine the caller's effective workspace role.
            $heldRoles = @()
            $evaluated = $true
            if ($Options -and $Options.ContainsKey('RoleAssignment')) {
                $heldRoles = @($Options.RoleAssignment | ForEach-Object {
                    if ($_ -is [string]) { $_ }
                    elseif ($_.Role) { $_.Role }
                    elseif ($_.RoleDefinitionName) { $_.RoleDefinitionName }
                })
                Write-PSFMessage -Level Debug -Message "Fabric: evaluating against supplied role(s) [$($heldRoles -join ', ')] (offline)." -Tag 'PSAutoRBAC', 'Provider', 'Fabric'
            }
            elseif ($Scope -and $Scope -ne '/' -and $Scope -ne 'Tenant') {
                $uri = "$resourceUrl/v1/workspaces/$Scope/roleAssignments"
                Write-PSFMessage -Level Debug -Message "Fabric: listing role assignments for workspace '$Scope'." -Tag 'PSAutoRBAC', 'Provider', 'Fabric'
                $res = Invoke-RBACRestRequest -Context $Context -ResourceUrl $resourceUrl -Uri $uri
                if ($res.Success) {
                    $heldRoles = @($res.Content.value |
                        Where-Object { $_.principal.id -eq $CallerId -or $_.principal.userDetails.userPrincipalName -eq $CallerId } |
                        ForEach-Object { $_.role })
                    Write-PSFMessage -Level Debug -Message "Fabric: '$CallerId' holds workspace role(s) [$($heldRoles -join ', ')]." -Tag 'PSAutoRBAC', 'Provider', 'Fabric'
                }
                elseif ($res.IsAuthorizationError) {
                    # Cannot even read assignments -> caller almost certainly lacks the role.
                    Write-PSFMessage -Level Verbose -Message "Fabric: authorization error reading assignments for '$Scope'; treating as no access." -Tag 'PSAutoRBAC', 'Provider', 'Fabric'
                    $heldRoles = @()
                }
                else {
                    $evaluated = $false
                    Write-PSFMessage -Level Warning -Message "Fabric: could not evaluate access for workspace '$Scope': $($res.Message)" -Tag 'PSAutoRBAC', 'Provider', 'Fabric'
                }
            }
            else {
                $evaluated = $false
                Write-PSFMessage -Level Warning -Message 'Fabric: access evaluation needs a workspace id in -Scope, or -RoleAssignment for offline checks.' -Tag 'PSAutoRBAC', 'Provider', 'Fabric'
            }

            $heldRank = ($heldRoles | ForEach-Object { [int]$rank[$_] } | Measure-Object -Maximum).Maximum
            if (-not $heldRank) { $heldRank = 0 }

            foreach ($role in $RequiredRole) {
                $needRank = [int]$rank[$role]
                $has = if (-not $evaluated) { $null } elseif ($needRank -eq 0) { ($heldRoles -contains $role) } else { $heldRank -ge $needRank }
                [pscustomobject]@{
                    PSTypeName = 'PSAutoRBAC.AccessState'
                    Platform   = 'Microsoft Fabric'
                    CallerId   = $CallerId
                    Role       = $role
                    Scope      = $Scope
                    HasAccess  = $has
                }
            }
        }

        ProbeLive = {
            param($Command, $ArgumentList, $Scope, $Context)
            $map = Get-RBACKnowledgeBase -Name 'CommandRoleMap'
            $commands = $map['Microsoft Fabric']
            $key = $commands.Keys | Where-Object { $_ -eq $Command } | Select-Object -First 1
            if (-not $key) { $key = '*Default' }
            $entry = $commands[$key]
            [pscustomobject]@{
                PSTypeName  = 'PSAutoRBAC.Requirement'
                Platform    = 'Microsoft Fabric'; Command = $Command
                Roles       = @($entry.Roles); Permissions = @($entry.Actions); ScopeLevel = $entry.ScopeLevel
                Notes       = 'Fabric REST errors do not name the required role; requirement resolved from the knowledge base, not a live failure.'
                IsKnown     = [bool]$key; Source = 'KnowledgeBase'
            }
        }

        NewGrantScript = {
            param($CallerId, $Role, $Scope, $Options)
            $workspace = if ($Options -and $Options.ContainsKey('WorkspaceId')) { $Options.WorkspaceId } else { $Scope }
            $add = @"
# PSAutoRBAC: grant Fabric workspace role '$Role' to '$CallerId' on workspace '$workspace'.
# Run as a workspace Admin. Idempotent. CallerId must be the principal's object id.
`$base = 'https://api.fabric.microsoft.com/v1/workspaces/$workspace/roleAssignments'
`$token = (Get-AzAccessToken -ResourceUrl 'https://api.fabric.microsoft.com').Token
`$headers = @{ Authorization = "Bearer `$token" }
`$existing = (Invoke-RestMethod -Uri `$base -Headers `$headers).value |
    Where-Object { `$_.principal.id -eq '$CallerId' }
if (`$existing) { Write-Host "Already has '`$(`$existing.role)' on workspace '$workspace'." }
else {
    `$body = @{ principal = @{ id = '$CallerId'; type = 'User' }; role = '$Role' } | ConvertTo-Json
    Invoke-RestMethod -Uri `$base -Method POST -Headers `$headers -Body `$body -ContentType 'application/json'
    Write-Host "Granted '$Role' to '$CallerId' on workspace '$workspace'."
}
"@

            $remove = @"
# PSAutoRBAC: remove Fabric workspace role for '$CallerId' from workspace '$workspace'.
# Run as a workspace Admin. Idempotent.
`$base = 'https://api.fabric.microsoft.com/v1/workspaces/$workspace/roleAssignments'
`$token = (Get-AzAccessToken -ResourceUrl 'https://api.fabric.microsoft.com').Token
`$headers = @{ Authorization = "Bearer `$token" }
`$existing = (Invoke-RestMethod -Uri `$base -Headers `$headers).value |
    Where-Object { `$_.principal.id -eq '$CallerId' }
if (`$existing) {
    Invoke-RestMethod -Uri "`$base/`$(`$existing.id)" -Method DELETE -Headers `$headers
    Write-Host "Removed workspace role from '$CallerId'."
} else { Write-Host "No workspace role for '$CallerId' on '$workspace'." }
"@

            @{ AddScript = $add; RemoveScript = $remove }
        }
    }
}

Register-RBACProvider -Provider (New-FabricProbeProvider)