Public/Set-RBACAccess.ps1
|
function Set-RBACAccess { <# .SYNOPSIS Discovers, evaluates, and (optionally) grants the least-privilege RBAC a caller needs to run a command. .DESCRIPTION End-to-end orchestration over the probe providers: 1. Resolve the requirement (preflight, or -LiveProbe for Azure). 2. Test whether -CallerId already holds each required role at the scope. 3. Generate idempotent grant / revoke snippets. 4. With -WhatIf, only report. Otherwise grant missing roles, honouring -Confirm. Automatic granting is implemented for Azure (New-AzRoleAssignment); for Graph / Fabric / Purview the AddScript is returned for review and the result is marked Applied = $false, because those grants are intentionally not executed automatically. Designed for Zero Trust provisioning: a framework identity holding only the relevant *roleAssignments write* permission grants the caller exactly the roles an operation needs - and the RemoveScript revokes them after. .PARAMETER Platform Platform name or alias. .PARAMETER Command The command whose requirement is evaluated. .PARAMETER CallerId The identity to evaluate and grant. .PARAMETER Scope / -SubscriptionId / -ResourceGroupName / -ManagementGroupId / -ResourceId Scope, or the parts to build an ARM scope from. .PARAMETER ArgumentList Arguments for -LiveProbe (and recorded on output). .PARAMETER LiveProbe Derive the requirement from a live Azure failure (Azure only). .PARAMETER IncludeGuard Also evaluate the guardrails (gates / blocks) that could prohibit the action or the role grant, attaching them to each result's Guards property. .PARAMETER GuardLive Detect guardrails against the live control plane (read-only), implying -IncludeGuard. A live 'Blocking' guard that affects the role assignment (a deny assignment, a ReadOnly lock, a policy deny on roleAssignments) causes Set-RBACAccess to skip the automatic grant and report Blocked = $true, so the framework never attempts a grant a guardrail would reject. .PARAMETER RoleAssignment Pre-fetched assignments/roles for offline evaluation. .PARAMETER Options Provider hashtable (e.g. @{ WorkspaceId='...' }). .PARAMETER MapPath Alternate knowledge-base path (testing). .PARAMETER TenantId / -RunAs* Probe-identity context (see Get-RBACRequirement). .EXAMPLE Set-RBACAccess -Platform Azure -SubscriptionId SUB1 -CallerId a@b.com ` -Command New-AzResourceGroup -WhatIf .OUTPUTS PSCustomObject (PSAutoRBAC.AccessResult) per required role. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] [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()] [switch]$IncludeGuard, [Parameter()] [switch]$GuardLive, [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 $commandName = if ($Command -is [scriptblock]) { '<scriptblock>' } else { [string]$Command } Write-PSFMessage -Level Verbose -Message "Set-RBACAccess: platform '$($provider.Name)', command '$commandName', caller '$CallerId'." -Tag 'PSAutoRBAC', 'Public', 'Access' try { $resolvedScope = Resolve-RBACScope -Scope $Scope -SubscriptionId $SubscriptionId ` -ResourceGroupName $ResourceGroupName -ManagementGroupId $ManagementGroupId ` -ResourceId $ResourceId -AllowTenantRoot $opts = @{} if ($Options) { $opts = $Options.Clone() } if ($MapPath) { $opts['MapPath'] = $MapPath } if ($LiveProbe -and $provider.SupportsLiveProbe -and $PSCmdlet.ShouldProcess("$commandName (as probe identity)", 'Execute command to derive RBAC requirement (LiveProbe)')) { $requirement = & $provider.ProbeLive $Command $ArgumentList $resolvedScope $context } else { $requirement = & $provider.ResolveRequirement $commandName $context $opts } $roles = @($requirement.Roles | Where-Object { $_ }) $testOpts = $opts.Clone() if ($PSBoundParameters.ContainsKey('RoleAssignment')) { $testOpts['RoleAssignment'] = $RoleAssignment } $states = if ($roles.Count) { @(& $provider.TestAccess $CallerId $roles $resolvedScope $context $testOpts) } else { @() } # Guardrails - a live 'Blocking' guard affecting the role assignment stops # the automatic grant, so the framework never attempts what would be rejected. $guards = @() $blockingGrantGuards = @() if ($IncludeGuard -or $GuardLive) { $guardOpts = $opts.Clone() if ($GuardLive) { $guardOpts['Live'] = $true } $guards = @(Invoke-RBACProviderGuard -Provider $provider -Command $commandName -CallerId $CallerId -Scope $resolvedScope -Context $context -Options $guardOpts) $blockingGrantGuards = @($guards | Where-Object { $_.Status -eq 'Blocking' -and ($_.Affects -eq 'RoleAssignment' -or $_.Affects -eq 'Both') }) if ($blockingGrantGuards.Count -gt 0) { Write-PSFMessage -Level Warning -Message "Set-RBACAccess: $($blockingGrantGuards.Count) blocking guardrail(s) [$(@($blockingGrantGuards.GuardType) -join ', ')] at '$resolvedScope' would reject a role assignment; grants will be skipped." -Tag 'PSAutoRBAC', 'Public', 'Access', 'Guard' } } foreach ($state in $states) { $scripts = & $provider.NewGrantScript $CallerId $state.Role $resolvedScope $opts $applied = $false $blocked = $blockingGrantGuards.Count -gt 0 if ($state.HasAccess -ne $true) { Write-PSFMessage -Level Verbose -Message "Set-RBACAccess: '$CallerId' is missing '$($state.Role)' at '$resolvedScope'." -Tag 'PSAutoRBAC', 'Public', 'Access' if ($blocked) { Write-PSFMessage -Level Warning -Message "Set-RBACAccess: not granting '$($state.Role)' - a blocking guardrail would reject the assignment. Resolve the guardrail (see the Guards property) first." -Tag 'PSAutoRBAC', 'Public', 'Access', 'Guard' } elseif ($PSCmdlet.ShouldProcess("$CallerId at $resolvedScope", "Grant '$($state.Role)' on $($provider.Name)")) { if ($provider.Name -eq 'Azure' -and (Get-Command New-AzRoleAssignment -ErrorAction SilentlyContinue)) { $azp = @{ SignInName = $CallerId; RoleDefinitionName = $state.Role; Scope = $resolvedScope; ErrorAction = 'Stop' } if ($context.AzContext) { $azp['DefaultProfile'] = $context.AzContext } New-AzRoleAssignment @azp | Out-Null $applied = $true Write-PSFMessage -Level Significant -Message "Set-RBACAccess: granted '$($state.Role)' to '$CallerId' at '$resolvedScope'." -Tag 'PSAutoRBAC', 'Public', 'Access' } else { Write-PSFMessage -Level Warning -Message "Automatic grant is not performed for '$($provider.Name)'. Run the AddScript on the result from an identity with the necessary management permission." -Tag 'PSAutoRBAC', 'Public', 'Access' } } } else { Write-PSFMessage -Level Debug -Message "Set-RBACAccess: '$CallerId' already holds '$($state.Role)'; nothing to do." -Tag 'PSAutoRBAC', 'Public', 'Access' } [pscustomobject]@{ PSTypeName = 'PSAutoRBAC.AccessResult' Platform = $provider.Name Command = $commandName CallerId = $CallerId Scope = $resolvedScope Role = $state.Role HasAccess = $state.HasAccess Applied = $applied Blocked = $blocked IsKnown = $requirement.IsKnown Source = $requirement.Source Permissions = $requirement.Permissions Notes = $requirement.Notes Guards = $guards AddScript = $scripts.AddScript RemoveScript = $scripts.RemoveScript } } } finally { if ($context.IsRunAs) { & $context.Disconnect } } } |