TL;DR
List Azure role assignments and custom role definitions recursively with PowerShell and Azure CLI.
Azure role-based access control (Azure RBAC) helps you manage who has access to Azure resources, what they can do with those resources, and what areas they have access to. Using RBAC in Azure for granular permissions makes it easy to assign permissions to users, groups, service principals, or managed identities. You can assign only the amount of access that users need to perform their jobs, thereby adhering to the principle of least privilege.
You have a ton of builtin roles to choose from, and you can also create your own custom roles if none of the builtin roles fit your use case.
I will not write a thesis on Azure RBAC, as you can find the necessary information on the Azure RBAC documentation page. I will, however, highlight a few shortcomings and how I worked around some of them.
You can list role definitions in the portal, with Azure CLI, or PowerShell.
All these links read List all roles. That is a bit misleading, as they only list the roles in your current scope with any inherited from above (management groups). Any custom roles created in different subscriptions than the current one (or the one provided in scope parameter) will not be listed. A best practice is to create custom roles higher up in management groups so that they are inherited by all subscriptions below. This is not always done, and you might end up with custom roles in different subscriptions.
You can list role assignments in the portal, with PowerShell, or with Azure CLI. There are different ways of listing role assignments, but no way to list all role assignments in your hierarchy recursively. You can list role assignments at a certain scope, with inherited assignments included. You can also find all role assignments for a specific user or group in Azure AD.
As far as I can see, there are a few shortcomings. These are not critical, and there are other issues with the RBAC model, but I will not go into them here.
Recently I was tasked with cleaning some clickOps’ed custom role definitions and converting them to Terraform. I needed to find all custom role definitions and all role assignments in all subscriptions in all management groups. I also needed to find all role assignments using the custom role definitions I was going to delete. Because of reasons I needed to create new role definitions, and could not import them into Terraform. Because of the shortcomings mentioned above, I had to write a script to list all role definitions and role assignments for all scopes.
I did not want to click through all of the subscriptions and management groups, so I wrote a script to do it for me.
At this point I would be remiss not to mention the Azure Governance Visualizer. It is a great tool created by Julian Hayward for visualizing your total Azure Governance. It lists all custom role definitions and every other detail you would need from your environment regarding RBAC and lot of other useful information. In this case it is too complex, and I wanted to focus on the RBAC part. Anyway, check it out if you need a great tool for visualizing your Azure Governance.
The script can be found in all its glory in GitHub. I will explain the different sections below.
I did not want the script to force a login of both PowerShell and Azure CLI every time I ran it. Therefore I needed some logic to check for login status and login if necessary.
# Log in with PowerShell if not already
$pwshContext = Get-AzContext
while (!$pwshContext) {
Write-Host "Not logged in with PowerShell. Logging in."
Connect-AzAccount | Out-Null
$pwshContext = Get-AzContext
}
# Log in with Azure CLI if not already
$azContext = $(az account show)
while (!$azContext) {
Write-Host "Not logged in with Azure CLI. Logging in."
az login | Out-Null
$azContext = $(az account show)
}
# Set current subscription if provided in parameter
if ($subscription) {
Write-Host "Changing PowerShell context to subscription $subscription"
Set-AzContext -Subscription $subscription | Out-Null
Write-Host "Changing Azure CLI context to subscription $subscription"
az account set --subscription ($pwshContext.Subscription.Id) | Out-Null
}
# Create folder for role definition export if it does not exist
if (!(Test-Path $rolesFolder)) {
New-Item -ItemType Directory -Name $rolesFolder -Force
}
Since there could be several management groups in different levels, I need to recursively find the management groups to list all subscriptions.
$subscriptions = @()
if ($topLvlMgmtGrp) {
# Collect data from managementgroups
$mgmtGroups = Get-AzManagementGroup -GroupId $topLvlMgmtGrp -Expand -Recurse
$children = $true
while ($children) {
$children = $false
$firstrun = $true
foreach ($entry in $mgmtGroups) {
if ($firstrun) { Clear-Variable mgmtGroups ; $firstrun = $false }
if ($entry.Children.length -gt 0) {
# Add management group to data that is being looped throught
$children = $true
$mgmtGroups += $entry.Children
}
else {
if ($entry.Name.Length -eq 36) {
# Add subscription to output object
$subscriptions += New-Object -TypeName psobject -Property ([ordered]@{'DisplayName' = $entry.DisplayName; 'SubscriptionID' = $entry.Name })
}
}
}
}
}
else {
$subscriptions += New-Object -TypeName psobject -Property ([ordered]@{'DisplayName' = (Get-AzContext).Subscription.Name; 'SubscriptionID' = (Get-AzContext).Subscription.Id })
}
This part is a simple loop through all subscriptions and list all custom role definitions. I could have used the PowerShell cmdlet Get-AzRoleDefinition
, but I wanted to use the Azure CLI command az role definition list
to get some more relevant information. The other actions done for each subscription are also done in the same foreach loop.
foreach ($sub in $subscriptions) {
Write-Host "Processing $($sub.DisplayName)."
$roles = $(az role definition list --custom-role-only $customRolesOnly --scope "/subscriptions/$($sub.SubscriptionID)" | ConvertFrom-Json)
foreach ($role in $roles) {
if ($role.roleName -like "$($excludeRegexPattern)") {
Write-Host "$($role.roleName) excluded by regexpattern."
}
elseif ($role.name -notin $exported -and $role.roleName -notlike "$($excludeRegexPattern)") {
$fileName = $role.roleName.toLower() -replace "custom - ", "" -replace " ", "_" -replace "-", "_" -replace "/", "_"
Write-Host "Exporting $($role.roleName) to file..."
$role | ConvertTo-Json -Depth 15 | out-file "$rolesFolder/role_definition_$($fileName).json" -encoding "utf8"
$exported += $role.name
}
else {
Write-Host "$($role.roleName) already exported."
}
...
}
}
This part is a simple loop through all custom roles in the current subscription and list all assignments. Exports them if required with exportAssignments
parameter.
foreach ($sub in $subscriptions) {
Write-Host "Processing $($sub.DisplayName)."
$roles = $(az role definition list --custom-role-only $customRolesOnly --scope "/subscriptions/$($sub.SubscriptionID)" | ConvertFrom-Json)
...
foreach ($role in $roles) {
if ($exportAssignments) {
$assignments = Get-AzRoleAssignment -Scope "/subscriptions/$($sub.SubscriptionID)" | Where-Object { $_.RoleDefinitionId -eq $role.name }
foreach ($ass in $assignments) {
$assignmentsList += [PSCustomObject]@{
RoleDefinitionId = $ass.RoleDefinitionId
RoleDefinitionName = $ass.RoleDefinitionName
AssignedSubscription = $sub.SubscriptionID
AssignmentId = $ass.RoleAssignmentId
ObjectId = $ass.ObjectId
SignInName = $ass.SignInName
DisplayName = $ass.DisplayName
Description = $ass.Description
Scope = $ass.Scope
}
}
}
}
}
This part is a simple conversion from PowerShell objects to json with ConvertTo-Json
and dumpt to json file.
if ($exportAssignments) {
Write-Host "Exporting assignments"
$assignmentsList | ConvertTo-Json | Out-File assignments.json -Force
}
Some parameters are necessary in this script to make it dynamic.
[String]
Id of your top level management group to start recursive listing.[String]
Set to true
if exporting only custom roles. Defaults to true
.[String]
Any exclusion RegEx pattern to use. Remember escape chars![String]
Folder where role definitions will be exported. Defaults to output
.[Switch]
Whether to export assignments to file or not.[String]
Subscription Id or name for when exporting in a single subscription.Running the script results in some output to json files.
It makes sense to only export custom role definitions, because the builtin ones are already pretty well documented.
For each custom role definition found, one file will be written. This is an example role and all guids are randomly generated.
{
"assignableScopes": [
"/subscriptions/effb9cb6-6226-43a6-a53c-2b78b39e9e9e/resourceGroups/<...>",
"/subscriptions/effb9cb6-6226-43a6-a53c-2b78b39e9e9e"
],
"description": "This is a sample role definition",
"id": "/subscriptions/effb9cb6-6226-43a6-a53c-2b78b39e9e9e/providers/Microsoft.Authorization/roleDefinitions/527f2931-a88f-4c44-b780-38e1a79f9d74",
"name": "527f2931-a88f-4c44-b780-38e1a79f9d74",
"permissions": [
{
"actions": [
"Microsoft.Network/*"
],
"dataActions": [],
"notActions": [],
"notDataActions": []
}
],
"roleName": "Network-Example-Role-Definition",
"roleType": "CustomRole",
"type": "Microsoft.Authorization/roleDefinitions"
}
All role assignments will be exported if the relevant parameter is set.
Output to a single assignments.json:
[
{
"RoleDefinitionId": "527f2931-a88f-4c44-b780-38e1a79f9d74",
"RoleDefinitionName": "Network-Example-Role-Definition",
"AssignedSubscription": "0f5d49e8-d9ca-47be-a895-cac7869513e6",
"AssignmentId": "/providers/Microsoft.Management/managementGroups/azureroot/providers/Microsoft.Authorization/roleAssignments/50985d62-443e-4485-adb4-d26bdef0b3b4",
"ObjectId": "283dde28-c4cb-4b1a-99c5-f10818c1dde5",
"SignInName": null,
"DisplayName": "some-demo-spn-name",
"Description": null,
"Scope": "/providers/Microsoft.Management/managementGroups/azureroot"
},
{
"RoleDefinitionId": "527f2931-a88f-4c44-b780-38e1a79f9d74",
"RoleDefinitionName": "Network-Example-Role-Definition",
"AssignedSubscription": "f5f7d4fc-08b9-4bda-ac8c-c810818b3b34",
"AssignmentId": "/providers/Microsoft.Management/managementGroups/azureroot/providers/Microsoft.Authorization/roleAssignments/50985d62-443e-4485-adb4-d26bdef0b3b4",
"ObjectId": "283dde28-c4cb-4b1a-99c5-f10818c1dde5",
"SignInName": null,
"DisplayName": "some-demo-spn-name",
"Description": null,
"Scope": "/providers/Microsoft.Management/managementGroups/azureroot"
},
{
"RoleDefinitionId": "527f2931-a88f-4c44-b780-38e1a79f9d74",
"RoleDefinitionName": "Network-Example-Role-Definition",
"AssignedSubscription": "62a59baf-d5aa-48a9-8c59-a22b85e89415",
"AssignmentId": "/providers/Microsoft.Management/managementGroups/azureroot/providers/Microsoft.Authorization/roleAssignments/50985d62-443e-4485-adb4-d26bdef0b3b4",
"ObjectId": "283dde28-c4cb-4b1a-99c5-f10818c1dde5",
"SignInName": null,
"DisplayName": "some-demo-spn-name",
"Description": null,
"Scope": "/providers/Microsoft.Management/managementGroups/azureroot"
}
]
I had some fun with this task, and maybe created an over engineered solution. Also I had the chance to practice my PowerShell-skills, which is a welcomed exercise!
Please let me know if you have a one-liner for this that I can use in the future 🙂