Public/Invoke-RBACProbe.ps1

function Invoke-RBACProbe {
    <#
    .SYNOPSIS
        Probes the RBAC a command needs and whether a caller already has it.
 
    .DESCRIPTION
        The flagship PSAutoRBAC operation. For a platform command it:
 
          1. Resolves the least-privilege requirement (provider preflight).
          2. Optionally derives the requirement from a *live* execution when
             -LiveProbe is supplied - only the Azure provider can do this, because
             only ARM names the missing action in its AuthorizationFailed error.
             Live probing is gated by ShouldProcess: although it prefers the
             command's own -WhatIf, it may reach the service and is therefore
             treated as a state-changing action.
          3. Tests whether the target -CallerId holds each required role at the
             scope (non-destructive).
          4. Emits, per required role, a combined result carrying the requirement,
             the access verdict, and idempotent grant/revoke snippets.
 
        Preflight (the default) makes no changes on any platform.
 
    .PARAMETER Platform
        Platform name or alias (Get-RBACProvider lists them).
 
    .PARAMETER Command
        The command / operation being probed.
 
    .PARAMETER CallerId
        The identity whose access is evaluated.
 
    .PARAMETER Scope
        Explicit scope. Overrides scope built from the Azure parameters below.
 
    .PARAMETER SubscriptionId
        Azure subscription id used to build an ARM scope.
 
    .PARAMETER ResourceGroupName
        Azure resource group used to build an ARM scope.
 
    .PARAMETER ManagementGroupId
        Azure management group used to build an ARM scope.
 
    .PARAMETER ResourceId
        Explicit ARM resource id scope.
 
    .PARAMETER ArgumentList
        Arguments passed to the command during -LiveProbe (and recorded on output).
 
    .PARAMETER LiveProbe
        Actually execute the command to derive the requirement from the live
        authorization failure. Azure only; ShouldProcess-gated.
 
    .PARAMETER RoleAssignment
        Pre-fetched assignments/roles for offline access evaluation.
 
    .PARAMETER Options
        Provider hashtable (e.g. @{ WorkspaceId='...'; Collection='...' }).
 
    .PARAMETER MapPath
        Alternate knowledge-base path (testing).
 
    .PARAMETER TenantId
        Optional tenant id.
 
    .PARAMETER RunAsCredential
        Probe as a user credential rather than the ambient session.
 
    .PARAMETER RunAsServicePrincipal
        Probe as a service principal (with -RunAsTenantId).
 
    .PARAMETER RunAsTenantId
        Tenant for service-principal run-as.
 
    .PARAMETER RunAsManagedIdentity
        Probe as a managed identity.
 
    .PARAMETER RunAsManagedIdentityClientId
        Client id for a user-assigned managed identity.
 
    .EXAMPLE
        Invoke-RBACProbe -Platform Azure -Command New-AzResourceGroup `
            -CallerId a@b.com -SubscriptionId SUB1
 
        Preflight: resolves Contributor, checks whether a@b.com holds it.
 
    .EXAMPLE
        Invoke-RBACProbe -Platform Azure -Command { New-AzStorageAccount ... } `
            -CallerId a@b.com -Scope /subscriptions/SUB1/resourceGroups/rg -LiveProbe
 
        Live: runs the command, parses the AuthorizationFailed, maps the action to
        a role.
 
    .OUTPUTS
        PSCustomObject (PSAutoRBAC.ProbeResult) per required role.
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Platform,

        [Parameter(Mandatory)]
        [ValidateNotNull()]
        [object]$Command,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$CallerId,

        [Parameter()]
        [string]$Scope,

        [Parameter()]
        [string]$SubscriptionId,

        [Parameter()]
        [string]$ResourceGroupName,

        [Parameter()]
        [string]$ManagementGroupId,

        [Parameter()]
        [string]$ResourceId,

        [Parameter()]
        [Alias('Args')]
        [object[]]$ArgumentList,

        [Parameter()]
        [switch]$LiveProbe,

        [Parameter()]
        [object[]]$RoleAssignment,

        [Parameter()]
        [hashtable]$Options,

        [Parameter()]
        [string]$MapPath,

        [Parameter()]
        [string]$TenantId,

        [Parameter()]
        [pscredential]$RunAsCredential,

        [Parameter()]
        [pscredential]$RunAsServicePrincipal,

        [Parameter()]
        [string]$RunAsTenantId,

        [Parameter()]
        [switch]$RunAsManagedIdentity,

        [Parameter()]
        [string]$RunAsManagedIdentityClientId
    )

    $provider = Get-RBACProviderInternal -Platform $Platform
    $context  = Initialize-RBACContext -BoundParameters $PSBoundParameters

    # A scriptblock command has no name; use a label for output/messages.
    $commandName = if ($Command -is [scriptblock]) { '<scriptblock>' } else { [string]$Command }
    Write-PSFMessage -Level Verbose -Message "Invoke-RBACProbe: platform '$($provider.Name)', command '$commandName', caller '$CallerId', live=$LiveProbe." -Tag 'PSAutoRBAC', 'Public', 'Probe'

    try {
        $resolvedScope = Resolve-RBACScope -Scope $Scope -SubscriptionId $SubscriptionId `
            -ResourceGroupName $ResourceGroupName -ManagementGroupId $ManagementGroupId `
            -ResourceId $ResourceId -AllowTenantRoot
        Write-PSFMessage -Level Debug -Message "Invoke-RBACProbe: resolved scope '$resolvedScope'." -Tag 'PSAutoRBAC', 'Public', 'Probe'

        $opts = @{}
        if ($Options) { $opts = $Options.Clone() }
        if ($MapPath) { $opts['MapPath'] = $MapPath }

        # 1/2. Requirement - preflight, or live for Azure.
        $mode = 'Preflight'
        if ($LiveProbe) {
            if (-not $provider.SupportsLiveProbe) {
                Write-PSFMessage -Level Warning -Message "Platform '$($provider.Name)' does not support live probing (its authorization error names no permission). Falling back to preflight resolution." -Tag 'PSAutoRBAC', 'Public', 'Probe'
                $requirement = & $provider.ResolveRequirement $commandName $context $opts
            }
            elseif ($PSCmdlet.ShouldProcess("$commandName (as probe identity)", 'Execute command to derive RBAC requirement (LiveProbe)')) {
                Write-PSFMessage -Level Significant -Message "Invoke-RBACProbe: executing live probe of '$commandName'." -Tag 'PSAutoRBAC', 'Public', 'Probe', 'LiveProbe'
                $requirement = & $provider.ProbeLive $Command $ArgumentList $resolvedScope $context
                $mode = 'LiveProbe'
            }
            else {
                Write-PSFMessage -Level Verbose -Message 'Invoke-RBACProbe: live probe declined via ShouldProcess; using preflight.' -Tag 'PSAutoRBAC', 'Public', 'Probe'
                $requirement = & $provider.ResolveRequirement $commandName $context $opts
            }
        }
        else {
            $requirement = & $provider.ResolveRequirement $commandName $context $opts
        }

        # 3. Access verdict.
        $roles = @($requirement.Roles | Where-Object { $_ })
        $testOpts = $opts.Clone()
        if ($PSBoundParameters.ContainsKey('RoleAssignment')) { $testOpts['RoleAssignment'] = $RoleAssignment }

        $states = @()
        if ($roles.Count -gt 0) {
            $states = @(& $provider.TestAccess $CallerId $roles $resolvedScope $context $testOpts)
        }

        # 4. Per-role combined result.
        if ($states.Count -eq 0) {
            [pscustomobject]@{
                PSTypeName   = 'PSAutoRBAC.ProbeResult'
                Platform     = $provider.Name
                Command      = $commandName
                CallerId     = $CallerId
                Scope        = $resolvedScope
                Role         = $null
                HasAccess    = $null
                Mode         = $mode
                IsKnown      = $requirement.IsKnown
                Source       = $requirement.Source
                Permissions  = $requirement.Permissions
                Notes        = $requirement.Notes
                AddScript    = $null
                RemoveScript = $null
            }
            return
        }

        foreach ($state in $states) {
            $scripts = & $provider.NewGrantScript $CallerId $state.Role $resolvedScope $opts
            [pscustomobject]@{
                PSTypeName   = 'PSAutoRBAC.ProbeResult'
                Platform     = $provider.Name
                Command      = $commandName
                CallerId     = $CallerId
                Scope        = $resolvedScope
                Role         = $state.Role
                HasAccess    = $state.HasAccess
                Mode         = $mode
                IsKnown      = $requirement.IsKnown
                Source       = $requirement.Source
                Permissions  = $requirement.Permissions
                Notes        = $requirement.Notes
                AddScript    = $scripts.AddScript
                RemoveScript = $scripts.RemoveScript
            }
        }
    }
    finally {
        if ($context.IsRunAs) { & $context.Disconnect }
    }
}