Skip to content

Commit 31451a9

Browse files
feat: improve role definition clean up and add exclusion filters (#495)
# Pull Request ## Description This PR updates the role assignment query to use resouce graph due to a bug with the az cli functions. It also adds two filter to specify which child management groups and which role assignments to delete. These are to enable the use of this function at scale for e2e testing. ## License By submitting this pull request, I confirm that my contribution is made under the terms of the projects associated license.
1 parent 1a28e70 commit 31451a9

File tree

1 file changed

+95
-65
lines changed

1 file changed

+95
-65
lines changed

src/ALZ/Public/Remove-PlatformLandingZone.ps1

Lines changed: 95 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,22 @@ function Remove-PlatformLandingZone {
106106
This is useful when you want to preserve custom role definitions or lack the necessary permissions to delete them.
107107
Default: $false (delete custom role definitions)
108108
109+
.PARAMETER ManagementGroupsToDeleteNamePatterns
110+
An array of wildcard patterns for management group names that should be deleted. Only management groups at the
111+
first level below the target management groups matching any of these patterns will be deleted. If the array is
112+
empty, all child management groups will be deleted (default behavior). Each pattern is evaluated using a -like
113+
expression with wildcards at the start and end (e.g., a pattern of "landingzone" will match management groups
114+
containing "landingzone" anywhere in their name).
115+
Default: Empty array (delete all child management groups)
116+
117+
.PARAMETER RoleDefinitionsToDeleteNamePatterns
118+
An array of wildcard patterns for custom role definition names that should be deleted. Only role definitions
119+
matching any of these patterns will be deleted during the custom role definition cleanup process. If the array
120+
is empty, all custom role definitions will be deleted (default behavior). Each pattern is evaluated using a
121+
-like expression with wildcards at the start and end (e.g., a pattern of "Custom" will match role definitions
122+
containing "Custom" anywhere in their name).
123+
Default: Empty array (delete all custom role definitions)
124+
109125
.EXAMPLE
110126
Remove-PlatformLandingZone -ManagementGroups @("alz-platform", "alz-landingzones")
111127
@@ -180,6 +196,18 @@ function Remove-PlatformLandingZone {
180196
Removes management groups and resource groups but skips custom role definition deletion. Useful when you want
181197
to preserve custom role definitions or lack the necessary permissions to delete them.
182198
199+
.EXAMPLE
200+
Remove-PlatformLandingZone -ManagementGroups @("alz-root") -ManagementGroupsToDeleteNamePatterns @("landingzone", "sandbox")
201+
202+
Removes only child management groups with names containing "landingzone" or "sandbox", preserving all other
203+
child management groups. This is useful when you want to selectively clean up specific management group branches.
204+
205+
.EXAMPLE
206+
Remove-PlatformLandingZone -ManagementGroups @("alz-test") -RoleDefinitionsToDeleteNamePatterns @("Test-Role", "Temporary")
207+
208+
Removes management groups and resource groups but only deletes custom role definitions with names containing
209+
"Test-Role" or "Temporary". Useful when you want to clean up specific custom roles while preserving others.
210+
183211
.NOTES
184212
This function uses Azure CLI commands and requires:
185213
- Azure CLI to be installed and available in the system path
@@ -240,7 +268,9 @@ function Remove-PlatformLandingZone {
240268
[switch]$SkipDefenderPlanReset,
241269
[switch]$SkipDeploymentDeletion,
242270
[switch]$SkipOrphanedRoleAssignmentDeletion,
243-
[switch]$SkipCustomRoleDefinitionDeletion
271+
[switch]$SkipCustomRoleDefinitionDeletion,
272+
[string[]]$ManagementGroupsToDeleteNamePatterns = @(),
273+
[string[]]$RoleDefinitionsToDeleteNamePatterns = @()
244274
)
245275

246276
function Write-ToConsoleLog {
@@ -376,11 +406,33 @@ function Remove-PlatformLandingZone {
376406
param (
377407
[object[]]$ManagementGroups,
378408
[int]$Depth = 0,
379-
[hashtable]$ManagementGroupsFound = @{}
409+
[hashtable]$ManagementGroupsFound = @{},
410+
[string[]]$ManagementGroupsToDeleteNamePatterns = @()
380411
)
381412

382413
$ManagementGroups = $ManagementGroups | Where-Object { $_.type -eq "Microsoft.Management/managementGroups" }
383414

415+
# Filter management groups at depth 0 (first level children) if patterns are specified
416+
if ($Depth -eq 0 -and $ManagementGroupsToDeleteNamePatterns.Count -gt 0) {
417+
$filteredManagementGroups = @()
418+
foreach($mg in $ManagementGroups) {
419+
$shouldDelete = $false
420+
foreach($pattern in $ManagementGroupsToDeleteNamePatterns) {
421+
if($mg.name -like "*$pattern*" -or $mg.displayName -like "*$pattern*") {
422+
Write-ToConsoleLog "Including management group for deletion due to pattern match '$pattern': $($mg.name) ($($mg.displayName))" -NoNewLine
423+
$shouldDelete = $true
424+
break
425+
}
426+
}
427+
if($shouldDelete) {
428+
$filteredManagementGroups += $mg
429+
} else {
430+
Write-ToConsoleLog "Skipping management group (no pattern match): $($mg.name) ($($mg.displayName))" -NoNewLine
431+
}
432+
}
433+
$ManagementGroups = $filteredManagementGroups
434+
}
435+
384436
foreach($managementGroup in $ManagementGroups) {
385437
if(!$ManagementGroupsFound.ContainsKey($Depth)) {
386438
$ManagementGroupsFound[$Depth] = @()
@@ -395,7 +447,7 @@ function Remove-PlatformLandingZone {
395447
if(!$ManagementGroupsFound.ContainsKey($Depth + 1)) {
396448
$ManagementGroupsFound[$Depth + 1] = @()
397449
}
398-
Get-ManagementGroupChildrenRecursive -ManagementGroups $children -Depth ($Depth + 1) -ManagementGroupsFound $ManagementGroupsFound
450+
Get-ManagementGroupChildrenRecursive -ManagementGroups $children -Depth ($Depth + 1) -ManagementGroupsFound $ManagementGroupsFound -ManagementGroupsToDeleteNamePatterns $ManagementGroupsToDeleteNamePatterns
399451
} else {
400452
Write-ToConsoleLog "Management group has no children: $($managementGroup.name)" -NoNewLine
401453
}
@@ -575,10 +627,10 @@ function Remove-PlatformLandingZone {
575627
param (
576628
[string]$ManagementGroupId,
577629
[string]$ManagementGroupDisplayName,
578-
[array]$Subscriptions,
579630
[int]$ThrottleLimit,
580631
[switch]$PlanMode,
581-
[string]$TempLogFileForPlan
632+
[string]$TempLogFileForPlan,
633+
[string[]]$RoleDefinitionsToDeleteNamePatterns = @()
582634
)
583635

584636
if(-not $PSCmdlet.ShouldProcess("Delete Custom Role Definitions", "delete")) {
@@ -596,92 +648,69 @@ function Remove-PlatformLandingZone {
596648
$_.assignableScopes -contains "/providers/Microsoft.Management/managementGroups/$ManagementGroupId"
597649
}
598650

651+
# Filter role definitions to only include those matching deletion patterns
652+
if ($RoleDefinitionsToDeleteNamePatterns -and $RoleDefinitionsToDeleteNamePatterns.Count -gt 0) {
653+
$filteredRoleDefinitions = @()
654+
foreach($roleDef in $customRoleDefinitions) {
655+
$shouldDelete = $false
656+
foreach($pattern in $RoleDefinitionsToDeleteNamePatterns) {
657+
if($roleDef.roleName -like "*$pattern*") {
658+
Write-ToConsoleLog "Including custom role definition for deletion due to pattern match '$pattern': $($roleDef.roleName) (ID: $($roleDef.name))" -NoNewLine
659+
$shouldDelete = $true
660+
break
661+
}
662+
}
663+
if($shouldDelete) {
664+
$filteredRoleDefinitions += $roleDef
665+
} else {
666+
Write-ToConsoleLog "Skipping custom role definition (no pattern match): $($roleDef.roleName) (ID: $($roleDef.name))" -NoNewLine
667+
}
668+
}
669+
$customRoleDefinitions = $filteredRoleDefinitions
670+
}
671+
599672
if (-not $customRoleDefinitions -or $customRoleDefinitions.Count -eq 0) {
600673
Write-ToConsoleLog "No custom role definitions found on management group: $ManagementGroupId ($ManagementGroupDisplayName), skipping." -NoNewLine
601674
return
602675
}
603676

604677
Write-ToConsoleLog "Found $($customRoleDefinitions.Count) custom role definition(s) on management group: $ManagementGroupId ($ManagementGroupDisplayName)" -NoNewLine
605678

606-
# For each custom role definition, find and delete all assignments first
679+
# For each custom role definition, find and delete all assignments using Resource Graph, then delete the definition
607680
foreach ($roleDefinition in $customRoleDefinitions) {
608681
Write-ToConsoleLog "Processing custom role definition: $($roleDefinition.roleName) (ID: $($roleDefinition.name))" -NoNewLine
609682

610-
# Find all role assignments for this custom role on the management group
611-
Write-ToConsoleLog "Checking for role assignments of custom role '$($roleDefinition.roleName)' on management group: $ManagementGroupId ($ManagementGroupDisplayName)" -NoNewLine
612-
$mgRoleAssignments = (az role assignment list --role $roleDefinition.roleName --scope "/providers/Microsoft.Management/managementGroups/$ManagementGroupId" --query "[].{id:id,principalName:principalName,principalId:principalId}" -o json) | ConvertFrom-Json
683+
# Use Resource Graph to find all role assignments for this custom role definition across all scopes
684+
$resourceGraphQuery = "authorizationresources | where type == 'microsoft.authorization/roleassignments' | where properties.roleDefinitionId == '/providers/Microsoft.Authorization/RoleDefinitions/$($roleDefinition.name)' | project id, name, properties"
685+
$roleAssignments = (az graph query -q $resourceGraphQuery --query "data" -o json) | ConvertFrom-Json
613686

614-
if ($mgRoleAssignments -and $mgRoleAssignments.Count -gt 0) {
615-
Write-ToConsoleLog "Found $($mgRoleAssignments.Count) role assignment(s) of custom role '$($roleDefinition.roleName)' on management group: $ManagementGroupId ($ManagementGroupDisplayName)" -NoNewLine
687+
if ($roleAssignments -and $roleAssignments.Count -gt 0) {
688+
Write-ToConsoleLog "Found $($roleAssignments.Count) role assignment(s) for custom role '$($roleDefinition.roleName)'" -NoNewLine
616689

617-
$mgRoleAssignments | ForEach-Object -Parallel {
690+
$roleAssignments | ForEach-Object -Parallel {
618691
$assignment = $_
619692
$roleDefinitionName = $using:roleDefinition.roleName
620-
$managementGroupId = $using:ManagementGroupId
621-
$managementGroupDisplayName = $using:ManagementGroupDisplayName
622693
$funcWriteToConsoleLog = $using:funcWriteToConsoleLog
623694
${function:Write-ToConsoleLog} = $funcWriteToConsoleLog
624695

625-
Write-ToConsoleLog "Deleting role assignment of custom role '$roleDefinitionName' for principal: $($assignment.principalName) ($($assignment.principalId)) from management group: $managementGroupId ($managementGroupDisplayName)" -NoNewLine
696+
Write-ToConsoleLog "Deleting role assignment '$($assignment.name)' of custom role '$roleDefinitionName' for principal: $($assignment.properties.principalId)" -NoNewLine
626697

627698
if($using:PlanMode) {
628699
Write-ToConsoleLog `
629-
"Deleting role assignment of custom role '$roleDefinitionName' for principal: $($assignment.principalName) ($($assignment.principalId)) from management group: $managementGroupId ($managementGroupDisplayName)", `
700+
"Deleting role assignment '$($assignment.name)' of custom role '$roleDefinitionName' for principal: $($assignment.properties.principalId)", `
630701
"Would run: az role assignment delete --ids $($assignment.id)" `
631702
-IsPlan -LogFilePath $using:TempLogFileForPlan
632703
} else {
633704
$result = az role assignment delete --ids $assignment.id 2>&1
634705
if (!$result) {
635-
Write-ToConsoleLog "Deleted role assignment of custom role '$roleDefinitionName' from management group: $managementGroupId ($managementGroupDisplayName)" -NoNewLine
706+
Write-ToConsoleLog "Deleted role assignment '$($assignment.name)' of custom role '$roleDefinitionName'" -NoNewLine
636707
} else {
637-
Write-ToConsoleLog "Failed to delete role assignment of custom role '$roleDefinitionName' from management group: $managementGroupId ($managementGroupDisplayName)" -IsWarning -NoNewLine
708+
Write-ToConsoleLog "Failed to delete role assignment '$($assignment.name)' of custom role '$roleDefinitionName'" -IsWarning -NoNewLine
638709
}
639710
}
640711
} -ThrottleLimit $using:ThrottleLimit
641712
} else {
642-
Write-ToConsoleLog "No role assignments found for custom role '$($roleDefinition.roleName)' on management group: $ManagementGroupId ($ManagementGroupDisplayName)" -NoNewLine
643-
}
644-
645-
# Find all role assignments for this custom role on subscriptions under the management group
646-
if ($Subscriptions -and $Subscriptions.Count -gt 0) {
647-
Write-ToConsoleLog "Checking for role assignments of custom role '$($roleDefinition.roleName)' on subscriptions under management group: $ManagementGroupId ($ManagementGroupDisplayName)" -NoNewLine
648-
649-
$Subscriptions | ForEach-Object -Parallel {
650-
$subscription = $_
651-
$roleDefinition = $using:roleDefinition
652-
$managementGroupId = $using:ManagementGroupId
653-
$managementGroupDisplayName = $using:ManagementGroupDisplayName
654-
$funcWriteToConsoleLog = $using:funcWriteToConsoleLog
655-
${function:Write-ToConsoleLog} = $funcWriteToConsoleLog
656-
657-
Write-ToConsoleLog "Checking for role assignments of custom role '$($roleDefinition.roleName)' on subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine
658-
659-
$subRoleAssignments = (az role assignment list --role $roleDefinition.roleName --subscription $subscription.Id --query "[].{id:id,principalName:principalName,principalId:principalId}" -o json) | ConvertFrom-Json
660-
661-
if ($subRoleAssignments -and $subRoleAssignments.Count -gt 0) {
662-
Write-ToConsoleLog "Found $($subRoleAssignments.Count) role assignment(s) of custom role '$($roleDefinition.roleName)' on subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine
663-
664-
foreach ($assignment in $subRoleAssignments) {
665-
Write-ToConsoleLog "Deleting role assignment of custom role '$($roleDefinition.roleName)' for principal: $($assignment.principalName) ($($assignment.principalId)) from subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine
666-
667-
if($using:PlanMode) {
668-
Write-ToConsoleLog `
669-
"Deleting role assignment of custom role '$($roleDefinition.roleName)' for principal: $($assignment.principalName) ($($assignment.principalId)) from subscription: $($subscription.Name) (ID: $($subscription.Id))", `
670-
"Would run: az role assignment delete --ids $($assignment.id)" `
671-
-IsPlan -LogFilePath $using:TempLogFileForPlan
672-
} else {
673-
$result = az role assignment delete --ids $assignment.id 2>&1
674-
if (!$result) {
675-
Write-ToConsoleLog "Deleted role assignment of custom role '$($roleDefinition.roleName)' from subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine
676-
} else {
677-
Write-ToConsoleLog "Failed to delete role assignment of custom role '$($roleDefinition.roleName)' from subscription: $($subscription.Name) (ID: $($subscription.Id))" -IsWarning -NoNewLine
678-
}
679-
}
680-
}
681-
} else {
682-
Write-ToConsoleLog "No role assignments found for custom role '$($roleDefinition.roleName)' on subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine
683-
}
684-
} -ThrottleLimit $using:ThrottleLimit
713+
Write-ToConsoleLog "No role assignments found for custom role '$($roleDefinition.roleName)'" -NoNewLine
685714
}
686715

687716
# Now delete the custom role definition itself
@@ -691,7 +720,7 @@ function Remove-PlatformLandingZone {
691720
Write-ToConsoleLog `
692721
"Deleting custom role definition: $($roleDefinition.roleName) (ID: $($roleDefinition.name))", `
693722
"Would run: az role definition delete --name $($roleDefinition.name) --scope `"/providers/Microsoft.Management/managementGroups/$ManagementGroupId`"" `
694-
-IsPlan -LogFilePath $using:TempLogFileForPlan
723+
-IsPlan -LogFilePath $TempLogFileForPlan
695724
} else {
696725
$result = az role definition delete --name $roleDefinition.name --scope "/providers/Microsoft.Management/managementGroups/$ManagementGroupId" 2>&1
697726
if (!$result) {
@@ -841,7 +870,8 @@ function Remove-PlatformLandingZone {
841870

842871
if($hasChildren -or $deleteTargetManagementGroups) {
843872
${function:Get-ManagementGroupChildrenRecursive} = $using:funcGetManagementGroupChildrenRecursive
844-
$managementGroupsToDelete = Get-ManagementGroupChildrenRecursive -ManagementGroups @($targetManagementGroups)
873+
$patternsToUse = $deleteTargetManagementGroups ? @() : $using:ManagementGroupsToDeleteNamePatterns
874+
$managementGroupsToDelete = Get-ManagementGroupChildrenRecursive -ManagementGroups @($targetManagementGroups) -ManagementGroupsToDeleteNamePatterns $patternsToUse
845875
} else {
846876
Write-ToConsoleLog "Management group has no children: $managementGroupId ($managementGroupDisplayName)" -NoNewLine
847877
}
@@ -1182,10 +1212,10 @@ function Remove-PlatformLandingZone {
11821212
Remove-CustomRoleDefinitionsForScope `
11831213
-ManagementGroupId $managementGroupId `
11841214
-ManagementGroupDisplayName $managementGroupDisplayName `
1185-
-Subscriptions $using:subscriptionsFinal `
11861215
-ThrottleLimit $using:ThrottleLimit `
11871216
-PlanMode:$using:PlanMode `
1188-
-TempLogFileForPlan $using:TempLogFileForPlan
1217+
-TempLogFileForPlan $using:TempLogFileForPlan `
1218+
-RoleDefinitionsToDeleteNamePatterns $using:RoleDefinitionsToDeleteNamePatterns
11891219

11901220
} -ThrottleLimit $ThrottleLimit
11911221
} else {

0 commit comments

Comments
 (0)