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. For Azure, B2B guest home email resolution is carried through to the tenant object ID used for the automatic role assignment. .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 $testOpts $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 = @{ RoleDefinitionName = $state.Role; Scope = $resolvedScope; ErrorAction = 'Stop' } if ($testOpts.ContainsKey('ResolvedObjectId')) { $azp['ObjectId'] = $testOpts.ResolvedObjectId } else { $azp['SignInName'] = $CallerId } 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 } } } |