codingfreaks

codingfreaks

Experiencing Microsoft

  • Archive
  • Tools
  • About
  • Privacy
  • RSS-Feed
  • Github
  • Youtube

Script to remove stale role assignments in Azure AD

Alexander Schmidt  |  April 18, 2022

If you remove an identity from Azure AD which had a role assignment to one of your resources the assignment will stay on the resource in a stale state. In this article I'll show how to remove those entries using PowerShell.


Summary

Maybe you already saw an entry like the following in the blade Access control (IAM) blade in the Azure Portal:

Image 1: Stale role assignement
Image 1: Stale role assignement

So basically what Identity not found means is that there is still a role assignment but the associated role assignment as been removed.

You can test this with the following script:

Listing 1
$group = New-AzResourceGroup -Name rg-stale-test -Location westeurope
$principal = New-AzAdServicePrincipal -DisplayName 'TEST'
New-AzRoleAssignment -ObjectId $principal.Id -Scope $group.ResourceId -RoleDefinitionName 'Reader'
Remove-AzADServicePrincipal -Id $principal.Id

You might wait a few seconds but after this you’ll get an Identity not found (stale) entry when you go to your resource group in the Azure Portal and click on Access control (IAM) and then on the Role Assignments tab.

Over time a lot of these entries can occur in your Azure tenant.

It is interesting to see Microsoft doing this in Azure again! Some readers may find this behavior familiar. It is in principle the same as looking into the security properties of a file or folder in Windows Explorer and seeing those S-1-... entries. Those are permissions in the Windows file system pointing to principals that no longer exist. I think this is odd especially in Azure. The entries serve no purpose AFAIK and should be removed together with the associated identity. At least there should be a warning when you remove the identity.

PowerShell Oneliner

Removing all stale assignments for a known resource is pretty easy using this PowerShell line:

Listing 2
Get-AzRoleAssignment -Scope $group.ResourceId | `
 Where-Object { $_.ObjectType -eq 'Unknown' } | `
  Remove-AzRoleAssignment

The scope points to the id of the resource the assignment was applied to. In my sample I can use $group.ResourceId because in listing 1 I stored the output of New-AzResourceGroup in a variable and can access its ResourceId now.

Automation

So knowing this it is clear that we should run this over all our resources and remove those entries automatically. To achieve this I came up with a script that utilizes Azure Resource Graph to collect all resource containers (subscriptions, resource groups, management groups) in a tenant and then iterates over them trying to remove all stale assignments.

I say “trying” because some of the containers may be secured by a no-delete resource lock. So the script checks that and informs me whenever it is not able to remove the role assignment.

In order to execute this script you need to install the module Az.ResourceGraph which is not part of the module collection Az in PowerShell.

Here is my script:

Listing 3
[CmdletBinding()]
param (
	[Parameter(Mandatory=$true)]
	[string]
	$TenantId
)
$containers = Search-AzGraph -Query "ResourceContainers | where tenantId == '$TenantId'"
$containerCount = $containers.Count
$current = 0
$removed = 0
$notRemoved = 0
$notRemovedIds = New-Object System.Collections.Generic.List[string]
foreach($container in $containers) {
	$current = $current + 1	
	Write-Host "Removing stale role assignments from [$($container.name)] ($current of $containerCount)..."
	$stales = Get-AzRoleAssignment -Scope $container.Id | Where-Object { $_.ObjectType -eq 'Unknown' }
	$amount = $stales.Count
	if ($amount -gt 0) {
		$deleteLocks = Get-AzResourceLock -Scope $container.Id | Where-Object { $_.Properties.level -eq 'CanNotDelete' }
		if ($deleteLocks.Count -gt 0) {
			Write-Host "Cannot remove $($amount) stale role assignments for [$($container.name)] because of existing no-delete-lock." -ForegroundColor Yellow
			$notRemoved = $notRemoved + $amount	
			$notRemovedIds.Add($container.Id)
			continue
		}
		$stales | Remove-AzRoleAssignment
		$removed = $removed + $amount				
		Write-Host "Removed $($amount)" -ForegroundColor Green
		continue
	}	
	Write-Host "Done"
}
Write-Host "Finished. Removed $removed stale role assignemnts." -ForegroundColor Green
if ($notRemovedIds.Count -gt 0) {
	Write-Host "Skipped removal of $notRemoved stale assignments for following resources:" -ForegroundColor Yellow
	foreach($id in $notRemovedIds) {
		Write-Host "- $id" -ForegroundColor Yellow
	}
}

Some of my readers may wonder why I filter my resource graph query on the tenant id at all. This is important in scenarios where you might have access to different tenants after you authorized your shell (see Azure Lighthouse).

A further improvement could be to utilize resource graph and iterate over all resources too because they too could have explicit role assignments (seeing this you now should see why this might be a bad idea actually!). Also removing and re-adding the locks might ne an option.


Alexander Schmidt

Written by Alexander Schmidt who lives and works in Magdeburg building useful things. You should follow him on Youtube