Providers/AzureProbeProvider.ps1
|
function New-AzureProbeProvider { <# .SYNOPSIS Constructs the Azure (ARM) RBAC probe provider. .DESCRIPTION Internal factory. Azure is the only supported platform whose authorization error reliably names the missing action and scope, so it is the only provider whose ProbeLive can derive requirements from a live failure. Preflight (ResolveRequirement + TestAccess) uses the offline knowledge base and Get-AzRoleAssignment / Get-AzRoleDefinition; live probing executes the command and parses the AuthorizationFailed response. .OUTPUTS PSCustomObject (PSAutoRBAC.Provider) #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Factory; constructs and returns a provider object, makes no state change.')] [OutputType([psobject])] param() $matchScope = { param($Assignment, $Scope) if (-not $Assignment.Scope) { return $false } $target = $Scope.TrimEnd('/') $held = ([string]$Assignment.Scope).TrimEnd('/') if ($held -eq '/') { return $true } # tenant root covers everything return $target.StartsWith($held, [System.StringComparison]::OrdinalIgnoreCase) } [pscustomobject]@{ PSTypeName = 'PSAutoRBAC.Provider' Name = 'Azure' Aliases = @('Azure PowerShell', 'Az', 'ARM', 'AzureRM', 'Azure CLI') SupportsLiveProbe = $true MatchScope = $matchScope ResolveRequirement = { param($Command, $Context, $Options) Write-PSFMessage -Level Verbose -Message "Azure: resolving requirement for '$Command'." -Tag 'PSAutoRBAC', 'Provider', 'Azure' $mapPath = if ($Options -and $Options.ContainsKey('MapPath')) { $Options.MapPath } else { $null } $map = if ($mapPath) { Get-RBACKnowledgeBase -Path $mapPath } else { Get-RBACKnowledgeBase -Name 'CommandRoleMap' } $platformKey = $map.Keys | Where-Object { $_ -eq 'Azure PowerShell' } | Select-Object -First 1 $commands = $map[$platformKey] $commandKey = $commands.Keys | Where-Object { $_ -eq $Command } | Select-Object -First 1 $isKnown = $true if (-not $commandKey) { $isKnown = $false; $commandKey = '*Default' Write-PSFMessage -Level Debug -Message "Azure: '$Command' not in knowledge base; using *Default." -Tag 'PSAutoRBAC', 'Provider', 'Azure' } $entry = $commands[$commandKey] Write-PSFMessage -Level Debug -Message "Azure: '$Command' -> role(s) [$(@($entry.Roles) -join ', ')] (known: $isKnown)." -Tag 'PSAutoRBAC', 'Provider', 'Azure' [pscustomobject]@{ PSTypeName = 'PSAutoRBAC.Requirement' Platform = 'Azure' 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 "Azure: testing '$CallerId' for role(s) [$(@($RequiredRole) -join ', ')] at '$Scope'." -Tag 'PSAutoRBAC', 'Provider', 'Azure' $assignments = $null if ($Options -and $Options.ContainsKey('RoleAssignment')) { $assignments = @($Options.RoleAssignment) Write-PSFMessage -Level Debug -Message "Azure: evaluating against $($assignments.Count) supplied assignment(s) (offline)." -Tag 'PSAutoRBAC', 'Provider', 'Azure' } else { if (-not (Get-Command -Name 'Get-AzRoleAssignment' -ErrorAction SilentlyContinue)) { Write-PSFMessage -Level Error -Message 'Azure: Get-AzRoleAssignment unavailable and no -RoleAssignment supplied.' -Tag 'PSAutoRBAC', 'Provider', 'Azure' throw 'Get-AzRoleAssignment is unavailable. Import Az.Resources (and Connect-AzAccount), or pass -RoleAssignment for offline evaluation.' } $common = @{ ErrorAction = 'Stop' } if ($Context -and $Context.AzContext) { $common['DefaultProfile'] = $Context.AzContext } try { Write-PSFMessage -Level Debug -Message "Azure: querying Get-AzRoleAssignment -SignInName '$CallerId'." -Tag 'PSAutoRBAC', 'Provider', 'Azure' $assignments = @(Get-AzRoleAssignment -SignInName $CallerId @common) } catch { Write-PSFMessage -Level Verbose -Message "Azure: SignInName lookup failed ($($_.Exception.Message)); retrying by ObjectId." -Tag 'PSAutoRBAC', 'Provider', 'Azure' $assignments = @(Get-AzRoleAssignment -ObjectId $CallerId @common) } } $scopeTrim = $Scope.TrimEnd('/') foreach ($role in $RequiredRole) { $match = $assignments | Where-Object { $held = ([string]$_.Scope).TrimEnd('/') $_.RoleDefinitionName -eq $role -and $held -and ($held -eq '/' -or $scopeTrim.StartsWith($held, [System.StringComparison]::OrdinalIgnoreCase)) } | Select-Object -First 1 [pscustomobject]@{ PSTypeName = 'PSAutoRBAC.AccessState' Platform = 'Azure' CallerId = $CallerId Role = $role Scope = $scopeTrim HasAccess = [bool]$match } } } ProbeLive = { param($Command, $ArgumentList, $Scope, $Context) Write-PSFMessage -Level Significant -Message "Azure: LIVE probing '$Command' at '$Scope' (this executes the command)." -Tag 'PSAutoRBAC', 'Provider', 'Azure', 'LiveProbe' $resolved = if ($Command -is [scriptblock]) { $Command } else { Get-Command -Name $Command -ErrorAction SilentlyContinue } if (-not $resolved) { Write-PSFMessage -Level Error -Message "Azure: cannot live-probe '$Command'; command not found in session." -Tag 'PSAutoRBAC', 'Provider', 'Azure', 'LiveProbe' throw "Cannot live-probe '$Command': the command is not available in this session. Import the owning module first." } $err = $null try { if ($resolved -is [scriptblock]) { & $resolved 2>&1 | Out-Null } else { $splat = @{} $params = if ($ArgumentList) { $ArgumentList } else { @() } # Prefer the command's own -WhatIf when it supports ShouldProcess and the # caller did not already pass one, to minimize side effects. if ($resolved.Parameters.ContainsKey('WhatIf') -and -not ($params -contains '-WhatIf')) { $splat['WhatIf'] = $true } & $resolved @params @splat 2>&1 | Out-Null } } catch { $err = $_ } if (-not $err) { Write-PSFMessage -Level Verbose -Message "Azure: live probe of '$Command' raised no authorization error." -Tag 'PSAutoRBAC', 'Provider', 'Azure', 'LiveProbe' return [pscustomobject]@{ PSTypeName = 'PSAutoRBAC.Requirement' Platform = 'Azure'; Command = $Command; Roles = @(); Permissions = @() ScopeLevel = 'Unknown' Notes = 'Live probe completed without an authorization error: the probe identity already has sufficient access, or the command did not reach ARM (e.g. -WhatIf short-circuited before the REST call).' IsKnown = $true; Source = 'LiveProbe' } } $parsed = ConvertFrom-AzAuthorizationError -InputObject $err if (-not $parsed.IsAuthorizationError) { Write-PSFMessage -Level Warning -Message "Azure: live probe of '$Command' failed with a non-authorization error; re-throwing." -Tag 'PSAutoRBAC', 'Provider', 'Azure', 'LiveProbe' throw $err } Write-PSFMessage -Level Significant -Message "Azure: derived required action(s) [$($parsed.Actions -join ', ')] from live AuthorizationFailed." -Tag 'PSAutoRBAC', 'Provider', 'Azure', 'LiveProbe' $match = ConvertTo-RBACRole -Action $parsed.Actions -Context $Context $derivedScope = if ($parsed.Scopes) { $parsed.Scopes[0] } else { $Scope } [pscustomobject]@{ PSTypeName = 'PSAutoRBAC.Requirement' Platform = 'Azure' Command = $Command Roles = @($match.Roles) Permissions = @($parsed.Actions) ScopeLevel = 'Resource' Notes = "Derived from live AuthorizationFailed at scope '$derivedScope' (role source: $($match.Source))." IsKnown = $true Source = 'LiveProbe' } } NewGrantScript = { param($CallerId, $Role, $Scope, $Options) $scope = $Scope.TrimEnd('/') $add = @" # PSAutoRBAC: grant '$Role' to '$CallerId' at scope '$scope'. # Run as an identity holding Microsoft.Authorization/roleAssignments/write # (RBAC Administrator, User Access Administrator, or Owner). Idempotent. Import-Module Az.Accounts -ErrorAction Stop Import-Module Az.Resources -ErrorAction Stop `$upn = '$CallerId'; `$role = '$Role'; `$scope = '$scope' `$existing = Get-AzRoleAssignment -SignInName `$upn -RoleDefinitionName `$role -Scope `$scope -ErrorAction SilentlyContinue if (`$existing) { Write-Host "Already assigned '`$role' to '`$upn' at '`$scope'." } else { New-AzRoleAssignment -SignInName `$upn -RoleDefinitionName `$role -Scope `$scope; Write-Host "Granted '`$role'." } "@ $remove = @" # PSAutoRBAC: revoke '$Role' from '$CallerId' at scope '$scope'. # Run as an identity holding Microsoft.Authorization/roleAssignments/delete. Idempotent. Import-Module Az.Accounts -ErrorAction Stop Import-Module Az.Resources -ErrorAction Stop `$upn = '$CallerId'; `$role = '$Role'; `$scope = '$scope' `$existing = Get-AzRoleAssignment -SignInName `$upn -RoleDefinitionName `$role -Scope `$scope -ErrorAction SilentlyContinue if (`$existing) { Remove-AzRoleAssignment -SignInName `$upn -RoleDefinitionName `$role -Scope `$scope; Write-Host "Revoked '`$role'." } else { Write-Host "No '`$role' assignment for '`$upn' at '`$scope'." } "@ @{ AddScript = $add; RemoveScript = $remove } } } } Register-RBACProvider -Provider (New-AzureProbeProvider) |