Providers/GraphProbeProvider.ps1
|
function Get-GraphCommandPermission { <# .SYNOPSIS Resolves the least-privilege Graph permissions for a cmdlet via the catalog. .DESCRIPTION Internal. Wraps Find-MgGraphCommand (the documented mechanism for finding a cmdlet's required permissions). Returns a hashtable @{ Permissions=[string[]]; Source=string } or $null when the catalog is unavailable or the command is unknown. .OUTPUTS System.Collections.Hashtable or $null #> [CmdletBinding()] param([Parameter(Mandatory)][string]$Command) if (-not (Get-Command -Name 'Find-MgGraphCommand' -ErrorAction SilentlyContinue)) { Write-PSFMessage -Level Debug -Message "Graph: Find-MgGraphCommand unavailable; will use knowledge-base fallback for '$Command'." -Tag 'PSAutoRBAC', 'Provider', 'Graph' return $null } try { $cmd = Find-MgGraphCommand -Command $Command -ErrorAction Stop | Select-Object -First 1 if (-not $cmd) { return $null } $perms = @($cmd.Permissions | Where-Object { $_ } | Select-Object -ExpandProperty Name -Unique) Write-PSFMessage -Level Debug -Message "Graph: catalog resolved '$Command' to permission(s) [$($perms -join ', ')]." -Tag 'PSAutoRBAC', 'Provider', 'Graph' return @{ Permissions = $perms; Source = 'GraphCommandCatalog' } } catch { Write-PSFMessage -Level Warning -Message "Graph: Find-MgGraphCommand failed for '$Command': $($_.Exception.Message)" -Tag 'PSAutoRBAC', 'Provider', 'Graph' return $null } } function New-GraphProbeProvider { <# .SYNOPSIS Constructs the Microsoft Graph RBAC probe provider. .DESCRIPTION Internal factory. Graph's authorization error ('Authorization_RequestDenied. Insufficient privileges to complete the operation.') does NOT name the missing permission, so error-parsing is useless here. Instead this provider resolves requirements authoritatively with Find-MgGraphCommand (Microsoft's documented mechanism for "what does this cmdlet need"), falling back to the offline knowledge base. Access is evaluated against the connected identity's consented scopes / app roles (Get-MgContext) and, for directory-role requirements, directory role membership. .OUTPUTS PSCustomObject (PSAutoRBAC.Provider) #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Factory; constructs and returns a provider object, makes no state change.')] [OutputType([psobject])] param() [pscustomobject]@{ PSTypeName = 'PSAutoRBAC.Provider' Name = 'Microsoft Graph' Aliases = @('Graph', 'Entra', 'MgGraph', 'AzureAD', 'Microsoft Entra') SupportsLiveProbe = $false MatchScope = { param($Assignment, $Scope) $true } # Graph permissions are tenant-wide ResolveRequirement = { param($Command, $Context, $Options) Write-PSFMessage -Level Verbose -Message "Graph: resolving requirement for '$Command'." -Tag 'PSAutoRBAC', 'Provider', 'Graph' $catalog = Get-GraphCommandPermission -Command $Command $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 Graph'] $commandKey = $commands.Keys | Where-Object { $_ -eq $Command } | Select-Object -First 1 $isKnown = [bool]$commandKey if (-not $commandKey) { $commandKey = '*Default' } $entry = $commands[$commandKey] $permissions = if ($catalog -and $catalog.Permissions.Count) { $catalog.Permissions } else { @($entry.Actions) } $source = if ($catalog -and $catalog.Permissions.Count) { $catalog.Source } else { 'KnowledgeBase' } if ($catalog -and $catalog.Permissions.Count) { $isKnown = $true } Write-PSFMessage -Level Debug -Message "Graph: '$Command' -> role(s) [$(@($entry.Roles) -join ', ')], permission(s) [$(@($permissions) -join ', ')] (source: $source)." -Tag 'PSAutoRBAC', 'Provider', 'Graph' [pscustomobject]@{ PSTypeName = 'PSAutoRBAC.Requirement' Platform = 'Microsoft Graph' Command = $Command Roles = @($entry.Roles) # least-privilege directory role(s) Permissions = $permissions # Graph API scopes / app roles ScopeLevel = 'Tenant' Notes = $entry.Notes IsKnown = $isKnown Source = $source } } TestAccess = { param($CallerId, $RequiredRole, $Scope, $Context, $Options) Write-PSFMessage -Level Verbose -Message "Graph: testing '$CallerId' for [$(@($RequiredRole) -join ', ')] (tenant-wide)." -Tag 'PSAutoRBAC', 'Provider', 'Graph' # Offline / explicit: caller passes the granted scopes or directory roles. $granted = $null if ($Options -and $Options.ContainsKey('RoleAssignment')) { $granted = @($Options.RoleAssignment | ForEach-Object { if ($_ -is [string]) { $_ } elseif ($_.RoleDefinitionName) { $_.RoleDefinitionName } else { "$_" } }) Write-PSFMessage -Level Debug -Message "Graph: evaluating against supplied scopes/roles [$($granted -join ', ')]." -Tag 'PSAutoRBAC', 'Provider', 'Graph' } elseif (Get-Command -Name 'Get-MgContext' -ErrorAction SilentlyContinue) { $ctx = Get-MgContext -ErrorAction SilentlyContinue if ($ctx) { $granted = @($ctx.Scopes) Write-PSFMessage -Level Debug -Message "Graph: evaluating against connected-identity scopes [$($granted -join ', ')]." -Tag 'PSAutoRBAC', 'Provider', 'Graph' } } if ($null -eq $granted) { Write-PSFMessage -Level Warning -Message 'Graph: no connected context and no -RoleAssignment; access reported as not held.' -Tag 'PSAutoRBAC', 'Provider', 'Graph' } foreach ($role in $RequiredRole) { $has = $false if ($null -ne $granted) { $has = [bool](@($granted) -contains $role) } [pscustomobject]@{ PSTypeName = 'PSAutoRBAC.AccessState' Platform = 'Microsoft Graph' CallerId = $CallerId Role = $role Scope = 'Tenant' HasAccess = $has } } } ProbeLive = { param($Command, $ArgumentList, $Scope, $Context) # Graph errors are non-diagnostic; we never derive a requirement from a # failure. Resolve authoritatively from the command catalog instead. $catalog = Get-GraphCommandPermission -Command $Command $perms = if ($catalog) { $catalog.Permissions } else { @() } [pscustomobject]@{ PSTypeName = 'PSAutoRBAC.Requirement' Platform = 'Microsoft Graph'; Command = $Command Roles = @(); Permissions = $perms; ScopeLevel = 'Tenant' Notes = 'Graph authorization errors do not name the missing permission; requirement resolved from Find-MgGraphCommand rather than a live failure.' IsKnown = [bool]$catalog; Source = 'GraphCommandCatalog' } } NewGrantScript = { param($CallerId, $Role, $Scope, $Options) $add = @" # PSAutoRBAC: assign Entra directory role '$Role' to '$CallerId'. # Run as Privileged Role Administrator (or Global Administrator). Idempotent. Import-Module Microsoft.Graph.Identity.DirectoryManagement -ErrorAction Stop Connect-MgGraph -Scopes 'RoleManagement.ReadWrite.Directory' -NoWelcome `$user = Get-MgUser -UserId '$CallerId' -ErrorAction Stop `$def = Get-MgRoleManagementDirectoryRoleDefinition -Filter "displayName eq '$Role'" | Select-Object -First 1 if (-not `$def) { throw "Directory role '$Role' not found." } `$held = Get-MgRoleManagementDirectoryRoleAssignment -Filter "principalId eq '`$(`$user.Id)' and roleDefinitionId eq '`$(`$def.Id)'" -ErrorAction SilentlyContinue if (`$held) { Write-Host "Already assigned '$Role' to '$CallerId'." } else { New-MgRoleManagementDirectoryRoleAssignment -PrincipalId `$user.Id -RoleDefinitionId `$def.Id -DirectoryScopeId '/' Write-Host "Assigned '$Role' to '$CallerId'." } "@ $remove = @" # PSAutoRBAC: remove Entra directory role '$Role' from '$CallerId'. # Run as Privileged Role Administrator (or Global Administrator). Idempotent. Import-Module Microsoft.Graph.Identity.DirectoryManagement -ErrorAction Stop Connect-MgGraph -Scopes 'RoleManagement.ReadWrite.Directory' -NoWelcome `$user = Get-MgUser -UserId '$CallerId' -ErrorAction Stop `$def = Get-MgRoleManagementDirectoryRoleDefinition -Filter "displayName eq '$Role'" | Select-Object -First 1 if (-not `$def) { throw "Directory role '$Role' not found." } `$held = Get-MgRoleManagementDirectoryRoleAssignment -Filter "principalId eq '`$(`$user.Id)' and roleDefinitionId eq '`$(`$def.Id)'" -ErrorAction SilentlyContinue if (`$held) { Remove-MgRoleManagementDirectoryRoleAssignment -UnifiedRoleAssignmentId `$held.Id; Write-Host "Removed '$Role'." } else { Write-Host "No '$Role' assignment for '$CallerId'." } "@ @{ AddScript = $add; RemoveScript = $remove } } } } Register-RBACProvider -Provider (New-GraphProbeProvider) |