Providers/PurviewProbeProvider.ps1

function New-PurviewProbeProvider {
    <#
    .SYNOPSIS
        Constructs the Microsoft Purview RBAC probe provider.
    .DESCRIPTION
        Internal factory. Purview governs data-plane access through metadata
        policies (collection-scoped roles such as Collection Admin, Data Source
        Admin, Data Curator, Data Reader), not the Azure action model, and its
        403s carry no role detail. This provider resolves requirements from the
        knowledge base and evaluates access (best effort) by reading the account's
        metadata policies via the Purview policystore REST API. Supply the account
        data-plane endpoint in -Scope (https://{account}.purview.azure.com) and,
        optionally, a collection name in -Options @{ Collection = '...' }.
    .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://purview.azure.net'

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

        ResolveRequirement = {
            param($Command, $Context, $Options)
            Write-PSFMessage -Level Verbose -Message "Purview: resolving requirement for '$Command'." -Tag 'PSAutoRBAC', 'Provider', 'Purview'
            $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 Purview']
            $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 Purview'
                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)
            $resourceUrl = 'https://purview.azure.net'

            Write-PSFMessage -Level Verbose -Message "Purview: testing '$CallerId' for [$(@($RequiredRole) -join ', ')] at '$Scope'." -Tag 'PSAutoRBAC', 'Provider', 'Purview'
            $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 "Purview: evaluating against supplied role(s) [$($heldRoles -join ', ')] (offline)." -Tag 'PSAutoRBAC', 'Provider', 'Purview'
            }
            elseif ($Scope -and $Scope -match '^https?://') {
                $endpoint = $Scope.TrimEnd('/')
                $uri = "$endpoint/policystore/metadataPolicies?api-version=2021-07-01"
                Write-PSFMessage -Level Debug -Message "Purview: reading metadata policies at '$endpoint'." -Tag 'PSAutoRBAC', 'Provider', 'Purview'
                $res = Invoke-RBACRestRequest -Context $Context -ResourceUrl $resourceUrl -Uri $uri
                if ($res.Success) {
                    # Walk decision rules; collect roles whose attribute rules reference the caller.
                    foreach ($policy in @($res.Content.values)) {
                        foreach ($rule in @($policy.properties.attributeRules)) {
                            $refsCaller = ($rule | ConvertTo-Json -Depth 30) -match [regex]::Escape($CallerId)
                            if ($refsCaller -and $rule.id -match ':role:(?<r>[^:]+)') {
                                $heldRoles += $Matches['r']
                            }
                        }
                    }
                    Write-PSFMessage -Level Debug -Message "Purview: '$CallerId' holds role(s) [$($heldRoles -join ', ')]." -Tag 'PSAutoRBAC', 'Provider', 'Purview'
                }
                elseif ($res.IsAuthorizationError) {
                    Write-PSFMessage -Level Verbose -Message "Purview: authorization error reading metadata policies at '$endpoint'; treating as no access." -Tag 'PSAutoRBAC', 'Provider', 'Purview'
                    $heldRoles = @()
                }
                else { $evaluated = $false; Write-PSFMessage -Level Warning -Message "Purview: could not read metadata policies at '$endpoint': $($res.Message)" -Tag 'PSAutoRBAC', 'Provider', 'Purview' }
            }
            else {
                $evaluated = $false
                Write-PSFMessage -Level Warning -Message 'Purview: access evaluation needs the account endpoint in -Scope (https://{account}.purview.azure.com), or -RoleAssignment for offline checks.' -Tag 'PSAutoRBAC', 'Provider', 'Purview'
            }

            foreach ($role in $RequiredRole) {
                $has = if (-not $evaluated) { $null } else { [bool](@($heldRoles) -contains $role) }
                [pscustomobject]@{
                    PSTypeName = 'PSAutoRBAC.AccessState'
                    Platform   = 'Microsoft Purview'
                    CallerId   = $CallerId
                    Role       = $role
                    Scope      = $Scope
                    HasAccess  = $has
                }
            }
        }

        ProbeLive = {
            param($Command, $ArgumentList, $Scope, $Context)
            $map = Get-RBACKnowledgeBase -Name 'CommandRoleMap'
            $commands = $map['Microsoft Purview']
            $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 Purview'; Command = $Command
                Roles       = @($entry.Roles); Permissions = @($entry.Actions); ScopeLevel = $entry.ScopeLevel
                Notes       = 'Purview 403s 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)
            $endpoint = if ($Options -and $Options.ContainsKey('Endpoint')) { $Options.Endpoint } else { $Scope }
            $collection = if ($Options -and $Options.ContainsKey('Collection')) { $Options.Collection } else { '<rootCollectionName>' }
            $add = @"
# PSAutoRBAC: grant Purview data-plane role '$Role' to '$CallerId' on collection '$collection'.
# Purview roles are granted by editing the collection's METADATA POLICY (policystore API),
# not by a simple role-assignment call. Run as a Collection Admin.
# 1) GET the metadata policy for the collection:
# GET $endpoint/policystore/metadataPolicies?api-version=2021-07-01&collectionName=$collection
# 2) In the returned policy, find the attributeRule whose id ends in ':role:$Role'
# (e.g. purviewmetadatarole_builtin_data-curator) and add the principal id
# '$CallerId' to its 'fromRule' attributeValueIncludes entries.
# 3) PUT the modified policy back:
# PUT $endpoint/policystore/metadataPolicies/{policyId}?api-version=2021-07-01
# See: https://learn.microsoft.com/purview/tutorial-metadata-policy-roles-apis
Write-Warning "Purview role grants require a metadata-policy edit; review and apply the steps above for '$Role' / '$CallerId'."
"@

            $remove = @"
# PSAutoRBAC: revoke Purview data-plane role '$Role' from '$CallerId' on collection '$collection'.
# Reverse of the grant: GET the metadata policy, remove '$CallerId' from the
# attributeRule for ':role:$Role', then PUT the policy back. Run as a Collection Admin.
Write-Warning "Purview role revokes require a metadata-policy edit; remove '$CallerId' from the '$Role' rule and PUT the policy back."
"@

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

Register-RBACProvider -Provider (New-PurviewProbeProvider)