Azure Security

Gain insights into your Azure role assignments on subscription level

Gain insights into your Azure role assignments on subscription level

TL;DR

List Azure role assignments and custom role definitions recursively with PowerShell and Azure CLI.

Jump to recipe

Azure Role-Based Access Control (RBAC)

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.

List Azure role definitions

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.

List Azure role assignments

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.

Shortcomings

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.

  • There is no central listing of role assignments for all scopes
  • There is no central listing of custom role definitions for all scopes
  • Role assignments and role definitions are not linked in any way other than in backend. If you try to delete a custom role definition still in use, you get an error message. You have to find all role assignments using the custom role definition and delete them first.
  • Role assignments and role definitions are not listed in Azure AD

The task

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.

Azure Governance Visualizer

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.

Principles

  • Log in with both Azure CLI and PowerShell
  • Recursively find all management groups and subscriptions
  • List all custom roles in all subscriptions
  • List all role assignments with relevant custom roles in all subscriptions
  • Write everything to json files for documentation or investigation

Prerequisites

  • A user with Reader role on the management group level to list all management groups.
  • A user with Reader role on the subscription level to list all subscriptions and their assignments/definitions.
  • Azure PowerShell installed
  • Azure CLI installed

The script

The script can be found in all its glory in GitHub. I will explain the different sections below.

Log in with both Azure CLI and PowerShell

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
}

Recursively find all management groups and subscriptions

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 })
}

List all custom roles in all subscriptions

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."
        }
        ...
    }
}

List all role assignments with relevant custom roles in all subscriptions

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
              }
          }
      }
    }
}

Write everything to json files for documentation or investigation

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
}

Tools used

  • Azure CLI
  • Azure PowerShell

Parameters

Some parameters are necessary in this script to make it dynamic.

  • topLvlMgmtGroup - [String] Id of your top level management group to start recursive listing.
  • customRolesOnly - [String] Set to true if exporting only custom roles. Defaults to true.
  • excludeRegexPattern - [String] Any exclusion RegEx pattern to use. Remember escape chars!
  • rolesFolder - [String] Folder where role definitions will be exported. Defaults to output.
  • exportAssignments - [Switch] Whether to export assignments to file or not.
  • subscription - [String] Subscription Id or name for when exporting in a single subscription.

Resulting json

Running the script results in some output to json files.

Role Definitions

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"
}

Role Assignments

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"
  }
]

In summary

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 🙂