Azure role-based access control (RBAC) offers powerful permission management, but over time, identities may get deleted — leaving behind orphaned role assignments. These “ghost permissions” clutter your access model and may lead to confusion or potential misconfiguration.
This script helps you automatically detect role assignments for deleted or unknown identities across management group, sub management groups and subscriptions belonging to that tree.
It’s a simple way to clean up and regain confidence in your access hygiene.
Why This Script Is Useful
- Clean up role assignments for users, apps, or groups that no longer exist
- Improve security posture by removing dangling permissions
- Generate audit reports for governance or compliance reviews
Usage
Make sure Azure CLI and PowerShell Core are installed
Run the script using the management group ID, either root or some specific management group (e.g., /providers/Microsoft.Management/managementGroups/root)
Review output either in terminal or a GridView (on Windows)
Run Example
.\Get-OrphanAssignments.ps1 -RootId "/providers/Microsoft.Management/managementGroups/root"
Script Code
You can find the maintained and updated script on GitHub:
- Library: https://github.com/Jiihaa/pws-helpers/
- Script Path: scripts/Get-OrphanAssignments.ps1
# Script to find all RBAC assignments to deleted identities in management groups and subscriptions
param(
[string]$RootId # ID of the Root Management Group (e.g., "/providers/Microsoft.Management/managementGroups/root")
)
# Login to Azure if not already logged in
(az account show --query id 2>&1) -match "az login" -and (az login) | Out-Null
Write-Host "Fetching all role assignments for deleted identities..."
$allRoleAssignments = @()
# Check role assignments for the root management group using the passed RootId
if (-not $RootId) {
Write-Host "Root management group ID was not provided." -ForegroundColor Red
} else {
Write-Host "Checking role assignments for root management group: $RootId"
try {
$rootMgmtGroupRoleAssignments = az role assignment list --scope $RootId --output json | ConvertFrom-Json
$allRoleAssignments += $rootMgmtGroupRoleAssignments
}
catch {
Write-Host "No subscriptions on root level." -ForegroundColor Yellow
}
}
# Fetch all other management groups
$managementGroups = az account management-group list --query '[].{name:name}' -o tsv
# Loop through each management group and fetch role assignments
foreach ($mg in $managementGroups) {
Write-Host "Fetching role assignments for management group '$mg'..."
$mgmtGroupScope = "/providers/Microsoft.Management/managementGroups/$mg"
$mgmtGroupRoleAssignments = az role assignment list --scope $mgmtGroupScope --output json | ConvertFrom-Json
$allRoleAssignments += $mgmtGroupRoleAssignments
# Fetch all subscriptions under the current management group
$subscriptions = az account management-group subscription show-sub-under-mg --name $mg --query '[].{id:id}' -o tsv
# Loop through each subscription and fetch role assignments
foreach ($subscriptionPath in $subscriptions) {
$subscriptionId = $subscriptionPath -replace '.*/subscriptions/([^/]+)', '$1'
Write-Host "Fetching role assignments for subscription '$subscriptionId'..."
$subscriptionScope = "/subscriptions/$subscriptionId"
try {
$subscriptionRoleAssignments = az role assignment list --scope $subscriptionScope --output json | ConvertFrom-Json
$allRoleAssignments += $subscriptionRoleAssignments
}
catch {
Write-Host "Error fetching role assignments for subscription '$subscriptionId'. Continuing to the next subscription..." -ForegroundColor Yellow
}
}
}
# Filter results to find deleted identities
$deletedAssignments = $allRoleAssignments | Where-Object {
($_ | Test-Path -Path 'principalId' -ErrorAction SilentlyContinue) -and
(
($_ | Test-Path -Path 'principalName' -eq $null) -or
($_ | Test-Path -Path 'principalType' -and ($_.principalType -eq "Unknown" -or $_.principalType -eq "Deleted"))
)
}
# Output results with specific fields
if ($deletedAssignments.Count -eq 0) {
Write-Host "No role assignments found for deleted identities in the root management group, other management groups, or subscriptions." -ForegroundColor Yellow
} else {
$filteredAssignments = $deletedAssignments | Select-Object principalId, principalType, roleDefinitionId, roleDefinitionName, scope
if ($IsWindows) {
$filteredAssignments | Out-GridView -Title "Role Assignments for Deleted Identities"
} else {
$filteredAssignments | Format-Table -AutoSize
}
}
Output Example
Principal ID Principal Type Role Scope
---------------------------------- --------------- ---------- --------------------------------------------
01234567-89ab-cdef-0123-456789abcdef Deleted Reader /subscriptions/xxx/resourceGroups/example-rg
89abcdef-0123-4567-89ab-cdef01234567 Unknown Contributor /providers/Microsoft.Management/managementGroups/my-mg
On Windows, this appears in a nice Out-GridView pop-up for sorting/filtering.
Things to Keep in Mind
Deleted entries may still be present due to replication delays
You still need permission to list RBAC at all scopes
Output helps prioritize cleanup, but changes are manual
Summary
Keeping your RBAC setup clean is part of Azure operational excellence. This script makes it much easier to spot and remove RBAC assignments to deleted or unknown identities, especially in large enterprise environments with multiple tenants, subscriptions, and management groups.