Private/RoleManagement/Azure/Get-AzureResourcePIMPolicy.ps1
|
function Get-AzureResourcePIMPolicy { <# .SYNOPSIS Retrieves Azure Resource PIM policy settings for a specific role. .DESCRIPTION Gets the actual PIM policy configuration for Azure Resource roles including activation requirements, maximum duration, approval settings, etc. .PARAMETER RoleDefinitionId The Azure role definition ID. .PARAMETER SubscriptionId The subscription ID where the role is assigned. .PARAMETER Scope The specific scope of the role assignment. .OUTPUTS PSCustomObject containing policy information #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$RoleDefinitionId, [Parameter()] [string]$SubscriptionId, [Parameter()] [string]$Scope ) Write-Verbose "Fetching Azure Resource PIM policy for role: $RoleDefinitionId in subscription: $SubscriptionId" try { # Azure Resource PIM policies are retrieved differently than Entra ID # They use the Azure Management API directly $context = Get-AzContext if (-not $context) { Write-Warning "No Azure context available for policy retrieval" return $null } # Get access token for Azure Management API $tokenObj = Get-AzAccessToken -ResourceUrl 'https://management.azure.com/' -ErrorAction Stop $token = if ($tokenObj.Token -is [System.Security.SecureString]) { [System.Net.NetworkCredential]::new('', $tokenObj.Token).Password } else { $tokenObj.Token } $policyApiVersion = '2020-10-01-preview' # Azure Resource PIM policy assignments are queried in the context of a scope, # but policy identity is role-based for this module's cache and UI behavior. $policyScope = if ($Scope) { $Scope } else { "/subscriptions/$SubscriptionId" } # Normalize role definition ID to GUID and construct correct path based on scope type $roleDefGuid = $RoleDefinitionId if ($roleDefGuid -match "/providers/Microsoft\.Authorization/roleDefinitions/([a-fA-F0-9\-]{36})") { $roleDefGuid = $matches[1] } $isManagementGroupScope = ($policyScope -match "^/providers/Microsoft\.Management/managementGroups/") # Prefer the original full ARM path from the schedule; only reconstruct when it is a bare GUID $roleDefPath = if ($RoleDefinitionId -match "^/") { $RoleDefinitionId } elseif ($isManagementGroupScope) { "/providers/Microsoft.Authorization/roleDefinitions/$roleDefGuid" } elseif ($SubscriptionId) { "/subscriptions/$SubscriptionId/providers/Microsoft.Authorization/roleDefinitions/$roleDefGuid" } else { "/providers/Microsoft.Authorization/roleDefinitions/$roleDefGuid" } # Call Azure REST API to get PIM role settings $headers = @{ 'Authorization' = "Bearer $token" 'Content-Type' = 'application/json' } # Helper: build a "policy unavailable" object so callers can distinguish missing data from defaults $unavailablePolicy = [PSCustomObject]@{ MaxDuration = 8 RequiresMfa = $false RequiresJustification = $true RequiresTicket = $false RequiresApproval = $false RequiresAuthenticationContext = $false AuthenticationContextId = $null AuthenticationContextDisplayName = $null AuthenticationContextDescription = $null AuthenticationContextDetails = $null NotificationSettings = $null ApprovalSettings = $null PolicyUnavailable = $true } $policyLookupScopes = [System.Collections.ArrayList]::new() $seenLookupScopes = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $AddPolicyLookupScope = { param([string]$CandidateScope) if ($CandidateScope -and $seenLookupScopes.Add($CandidateScope)) { $null = $policyLookupScopes.Add($CandidateScope) } } & $AddPolicyLookupScope $policyScope if ($policyScope -match '^(\/providers/Microsoft\.Management/managementGroups/[^\/]+)') { & $AddPolicyLookupScope $matches[1] } elseif ($policyScope -match '^(\/subscriptions\/[^\/]+)') { & $AddPolicyLookupScope $matches[1] } elseif ($SubscriptionId) { & $AddPolicyLookupScope "/subscriptions/$SubscriptionId" } if ($policyLookupScopes.Count -eq 0) { Write-Verbose "Get-AzureResourcePIMPolicy: no policy lookup scope available for '$roleDefGuid'; returning unavailable-policy marker." return $unavailablePolicy } # Helper: extract HTTP status code from a caught Invoke-RestMethod exception $GetHttpStatus = { param($err) try { if ($err.Exception.Response) { return [int]$err.Exception.Response.StatusCode } } catch {} return 0 } # Helper: parse policy rules into the standard policy object $ParseRules = { param($rules) $maxDuration = 8 $requiresMfa = $false $requiresJustification = $true $requiresTicket = $false $requiresApproval = $false $requiresAuthenticationContext = $false $authenticationContextId = $null foreach ($rule in $rules) { switch ($rule.id) { 'Expiration_EndUser_Assignment' { if ($rule.maximumDuration) { try { $ts = [System.Xml.XmlConvert]::ToTimeSpan($rule.maximumDuration) $maxDuration = [int]$ts.TotalHours } catch { if ($rule.maximumDuration -match 'PT(\d+)H') { $maxDuration = [int]$matches[1] } elseif ($rule.maximumDuration -match 'PT(\d+)M') { $maxDuration = [Math]::Max(1, [int]([int]$matches[1] / 60)) } } } } 'Enablement_EndUser_Assignment' { if ($rule.enabledRules) { $enabledArr = @($rule.enabledRules) $requiresJustification = 'Justification' -in $enabledArr $requiresMfa = 'MultiFactorAuthentication' -in $enabledArr $requiresTicket = 'Ticketing' -in $enabledArr } } 'Approval_EndUser_Assignment' { if ($rule.setting -and $null -ne $rule.setting.isApprovalRequired) { $requiresApproval = [bool]$rule.setting.isApprovalRequired } } 'AuthenticationContext_EndUser_Assignment' { if ($rule.isEnabled -eq $true) { $requiresAuthenticationContext = $true if ($rule.claimValue) { $authenticationContextId = $rule.claimValue } } } } } return [PSCustomObject]@{ MaxDuration = $maxDuration RequiresMfa = $requiresMfa RequiresJustification = $requiresJustification RequiresTicket = $requiresTicket RequiresApproval = $requiresApproval RequiresAuthenticationContext = $requiresAuthenticationContext AuthenticationContextId = $authenticationContextId AuthenticationContextDisplayName = $null AuthenticationContextDescription = $null AuthenticationContextDetails = $null PolicyUnavailable = $false } } $encodedRoleDefinitionId = [System.Uri]::EscapeDataString($roleDefPath) $filter = "roleDefinitionId%20eq%20'$encodedRoleDefinitionId'" foreach ($policyLookupScope in $policyLookupScopes) { $nextUri = "https://management.azure.com$policyLookupScope/providers/Microsoft.Authorization/roleManagementPolicyAssignments?api-version=$policyApiVersion&`$filter=$filter" $policyId = $null while ($nextUri -and -not $policyId) { $listResponse = $null try { $listResponse = Invoke-RestMethod -Uri $nextUri -Headers $headers -Method Get -ErrorAction Stop } catch { $httpStatus = & $GetHttpStatus $_ if ($httpStatus -in @(401, 403)) { Write-Verbose "Get-AzureResourcePIMPolicy: HTTP $httpStatus at '$policyLookupScope' - Microsoft.Authorization/roleManagementPolicies/read required (Reader role). Returning unavailable policy." return $unavailablePolicy } Write-Verbose "Get-AzureResourcePIMPolicy: policy assignment list failed at '$policyLookupScope' (HTTP $httpStatus): $($_.Exception.Message)" break } $matched = @($listResponse.value) | Where-Object { $rdId = $_.properties.roleDefinitionId ($rdId -eq $roleDefPath) -or ($rdId -match '/roleDefinitions/([a-fA-F0-9\-]{36})$' -and $matches[1] -eq $roleDefGuid) -or ($rdId -eq $roleDefGuid) } | Select-Object -First 1 if ($matched) { $policyId = $matched.properties.policyId } else { $nextUri = if ($listResponse.nextLink) { $listResponse.nextLink } else { $null } } } if (-not $policyId) { Write-Verbose "Get-AzureResourcePIMPolicy: no policy assignment matched '$roleDefGuid' at '$policyLookupScope'" continue } # policyId may be a full ARM path (/subscriptions/.../roleManagementPolicies/{name}) # or just the policy name. Handle both. $policyUri = if ($policyId -match '^/') { "https://management.azure.com${policyId}?api-version=$policyApiVersion" } else { "https://management.azure.com$policyLookupScope/providers/Microsoft.Authorization/roleManagementPolicies/${policyId}?api-version=$policyApiVersion" } $policyResponse = $null try { $policyResponse = Invoke-RestMethod -Uri $policyUri -Headers $headers -Method Get -ErrorAction Stop } catch { $httpStatus = & $GetHttpStatus $_ Write-Verbose "Get-AzureResourcePIMPolicy: policy fetch failed at '$policyLookupScope' (HTTP $httpStatus): $($_.Exception.Message)" continue } if ($policyResponse -and $policyResponse.properties -and $policyResponse.properties.rules) { Write-Verbose "Get-AzureResourcePIMPolicy: successfully parsed policy for '$roleDefGuid' at '$policyLookupScope'" return & $ParseRules -rules $policyResponse.properties.rules } } Write-Verbose "Get-AzureResourcePIMPolicy: could not retrieve policy for '$roleDefGuid' at scopes '$($policyLookupScopes -join ', ')'; returning unavailable-policy marker." return $unavailablePolicy } catch { Write-Verbose "Failed to retrieve Azure Resource PIM policy: $($_.Exception.Message)" return $unavailablePolicy } } |