Introduction

In this guide, we will delve into the intricacies of configuring Privileged Identity Management (PIM) Eligible Role Assignments on Azure subscriptions using the ARM API in PowerShell. As seasoned professionals, we recognize that leveraging PIM in Azure is a strategic imperative. However, as DevOps Engineers, we also acknowledge the challenges posed by incorporating Eligible role assignments into deployments. In this post I will expose all the intricacies concercing this piece of automation. Microsoft provides Micosoft Graph cmdlets for Entra ID PIM, but for Azure PIM Role Assignments you must use the Azure Resource Manager (ARM) API.

Before we dive into the details, I want to give a shout-out to my colleague Bjorn Peters. He did the refactoring of the JSON-body into a PowerShell object so it’s easier to manipulate the code. This provides with a cleaner looking script. Also check out his blog for interesting articles about Azure, DevOps and automation.

ARM API

As mentioned, Microsoft provides us with the Azure Resource Manager (ARM) API for configuring PIM assignments. While this API offers unparalleled flexibility, deciphering the precise configuration details can sometimes be like unraveling a labyrinth. In this blogpost I will provide code snippets, making automation of PIM eligible roles in your environment a breeze. As mentioned before, we will do everything in PowerShell including the body. Hopefully this will provide you with more insights on how to use the code snippets in your automation.

The requirements

  • An IDE (such as Visual Studio Code)
  • Proficiency in PowerShell
    • Azure PowerShell module
    • Microsoft Graph module
  • Entra ID Premium license for PIM
  • optional: Custom role definition

Configuration steps

PIM configuration exists of two steps:

  1. Define Role Settings: These settings determine when role activation occurs. Think of them as your compass.

role-settings

  1. Create Eligible Role Assignments: This step associates roles with users or groups, allowing temporary permission elevation using PIM.

Custom Role Definitions

Scope Matters: When creating custom role definitions, consider the scope. If you intend to target multiple subscriptions, create the custom role definition at the highest possible level. Why? Because each role definition has its own unique GUID. If you create a new GUID for every subscription, your automation complexity increases significantly.

Example Scenario: Imagine an enterprise aiming to restrict developer access. They define a custom role called Developer with GUID 3a9c9b30-bb02-43c2-a487-0d5aff050fec. Now, they want to assign this custom role via PIM to different security groups across various subscriptions. In this case, it’s prudent to create the role definition at a higher level, ensuring consistent GUIDs for role assignments across different levels.

Remember these principles as you want to use custom role definitions within PIM. It will streamline your development processes.

Visualization of PIM a custom role definition:

pim-role-assignment-levels

Tasks

In this section we are going to execute the following tasks:

no. Task
1. Connect to the environment
2. Connect with MG Graph. Used for the creation of groups
3. Create 2 new security groups. 1 for PIM requests, 1 for approval of requests
4. Create a basic function to obtain headers. Used for making API calls
5. Store recurring values in objects
6. Update role policy with a custom role settings
7. Assign the eligible role

In the end we are also going to test our setup.

Getting started

  1. Connect with you Azure environment Connect-AzAccount to get started.
  2. Connect with MG Graph with at least Group read/write permissions:
1
Connect-MgGraph -Scopes "Group.ReadWrite.All"
  1. Create a new Security Group that will be assigned the PIM Eligible role:
1
2
$pimRequestorGroup = New-MgGroup -DisplayName 'pim-requestor-sg' -MailEnabled:$False  -MailNickName 'pim-test-sg' -SecurityEnabled
$pimApproverGroup = New-MgGroup -DisplayName 'pim-approver-sg' -MailEnabled:$False  -MailNickName 'pim-test-sg' -SecurityEnabled
  1. Obtain headers. To be able to to execute calls to the ARM API we need to obtain the correct headers. We will wrap this in a function for easier use throughout the codebase:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Function Get-Headers {
    Param (
        [Parameter(Mandatory)]
        [Array]$Context
    )
    $azProfile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile
    $profileClient = New-Object -TypeName Microsoft.Azure.Commands.ResourceManager.Common.RMProfileClient -ArgumentList ($azProfile)
    $token = $profileClient.AcquireAccessToken($Context.Subscription.TenantId)
    $authHeader = @{
        'Content-Type'  = 'application/json'
        'Authorization' = 'Bearer ' + $token.AccessToken
    }
    return $authHeader
}

If we call the function and save the output in an object we can reuse the headers with every API call, like so:

1
$headers = Get-Headers -Context (Get-AzContext)
  1. Store reusable values in objects to use later and switch to the correct context. Find the role definition ID’s here.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$subscription = Get-AzSubscription -SubscriptionId 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' # > replace this with your own subscription ID
# Switch to the target subscription
Set-AzContext -Subscription $subscription

$eligibleAssignmentDetails = [PSCustomObject]@{
    Id               = $pimRequestorGroup.Id # Here we enter the pimRequestorGroup ID set earlier
    DisplayName      = $pimRequestorGroup.DisplayName # Here we enter the pimGroup DisplayName set earlier
    EligibleRole     = 'Contributor'
}

$contributorRoleId = 'b24988ac-6180-42a0-ab88-20f7382dd24c' # This is the targeted role
  1. Update the role policy so it requires an approval on activation.
1
2
3
# First, get the current role policy. We do this because you are only allowed to update role policies:
$getRolePolicyUri = "https://management.azure.com/providers/Microsoft.Subscription/subscriptions/{0}/providers/Microsoft.Authorization/roleManagementPolicies?api-version=2020-10-01&`$filter=roleDefinitionId%20eq%20'subscriptions/{0}/providers/Microsoft.Authorization/roleDefinitions/{1}'" -f $subscription.Id, $contributorRoleId
$rolePolicy = (Invoke-RestMethod -Uri $getRolePolicyUri -Method Get -Headers $headers).value

Now that we obtained the role policy, we need to create a body to update the policy to our liking. For reference checkout the docs. The body in PowerShell gives us more flexibility if we want to loop through multiple roles/groups/approvers etc. Here goes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# Assemble body for this request
$body = [PSCustomObject]@{
    properties = [PSCustomObject]@{
        rules = @(
            # Enter the basics
            [PSCustomObject]@{
                isExpirationRequired = $true
                maximumDuration      = 'PT8H' #Role can be activated for a maximum of 8 hours
                id                   = 'Expiration_EndUser_Assignment'
                ruleType             = 'RoleManagementPolicyExpirationRule'
                target               = @{
                    caller     = 'EndUser'
                    operations = @(
                        'All'
                    )
                }
                level                = 'Assignment'
            },
            [PSCustomObject]@{
                enabledRules = @(
                    'Justification', # Requires the user to add a justification in their request
                    'MultiFactorAuthentication' # Requires MFA authentication for the request
                )
                id           = 'Enablement_EndUser_Assignment'
                ruleType     = 'RoleManagementPolicyEnablementRule'
                target       = @{
                    caller     = 'EndUser'
                    operations = @(
                        'All'
                    )
                    level      = 'Assignment'
                }
            },
            [PSCustomObject]@{
                isExpirationRequired = $false # Makes the role permanently eligible
                maximumDuration      = 'P365D' # Maximum duration of eligible role assignment
                id                   = 'Expiration_Admin_Eligibility'
                ruleType             = 'RoleManagementPolicyExpirationRule'
                target               = @{
                    caller     = 'Admin'
                    operations = @(
                        'All'
                    )
                    level      = 'Eligibility'
                }
            },
            # The next section adds Approvers to the body object. If you don't want/need approvers you can leave this part out of your script:
            [PSCustomObject]@{
                setting  = [PSCustomObject]@{
                    isApprovalRequired               = $true # Makes approval required for the request on this role
                    isApprovalRequiredForExtension   = $false
                    isRequestorJustificationRequired = $true
                    approvalMode                     = 'SingleStage'
                    approvalStages                   = @(
                        @{
                            approvalStageTimeOutInDays      = 1
                            isApproverJustificationRequired = $true
                            escalationTimeInMinutes         = 0
                            isEscalationEnabled             = $false
                            primaryApprovers                = @(
                                [PSCustomObject]@{
                                    id          = $pimApproverGroup.Id # Reference to the Security Group that was created earlier
                                    description = $null
                                    isBackup    = $false
                                    userType    = "Group"
                                }
                            )
                        }
                    )
                }
                id       = 'Approval_EndUser_Assignment'
                ruleType = 'RoleManagementPolicyApprovalRule'
                target   = @{
                    caller     = 'EndUser'
                    operations = @(
                        'All'
                    )
                    level      = 'Assignment'
                }
            }
        )
    }
}

We have created the body, now it’s time to update the role policy! First we construct the Uri by inserting the Id we got from obtaining the role policy. Secondly we update the role policy by calling the API with a PATCH method. If all goes well the role policy should be updated.

1
2
        $patchRolePolicyUri = "https://management.azure.com{0}?api-version=2020-10-01" -f $rolePolicy.id
        $patchPolicyRequest = Invoke-RestMethod -Uri $patchRolePolicyUri -Method Patch -Headers $headers -Body ($body | ConvertTo-Json -Depth 10)

Before:

role-policy-before

After:

role-policy-after

  1. Assign the Eligible role to the pimRequestorGroup Security Group:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# Create the Eligible role with a custom GUID
# Create body
$body = @{
    Properties = @{
        RoleDefinitionID = "/subscriptions/$Subscription.Id/providers/Microsoft.Authorization/roleDefinitions/$contributorRoleId"
        PrincipalId      = $pimRequestorGroup.Id
        RequestType      = 'AdminAssign'
        ScheduleInfo     = @{
            Expiration = @{
                Type = 'NoExpiration'
            }
        }
    }
}
$guid = [guid]::NewGuid()
# Construct Uri with subscription Id and new GUID
$createEligibleRoleUri = "https://management.azure.com/providers/Microsoft.Subscription/subscriptions/{0}/providers/Microsoft.Authorization/roleEligibilityScheduleRequests/{1}?api-version=2020-10-01" -f $Subscription.Id, $guid

# Call the API with PUT to assign the role to the targeted Security Group
Invoke-RestMethod -Uri $createEligibleRoleUri -Method Put -Headers $headers -Body ($body | ConvertTo-Json -Depth 10)

Result:

role-assignment-after

Testing!

All is now in place to test the setup. We followed the steps to create 2 security groups, update a role policy, assign the eligible role and finally we need to test this configuration:

Group Membership

Add members to the created groups:

Remember, users cannot approve their own requests.

  • pimRequestorGroup: Users who can request activation of the Contributor role.

pim-requestor

  • pimApprovalGroup: Approvers responsible for granting or denying requests.

pim-approver

Test the workflow

Test the setup with the separate accounts:

Requestor

Make a PIM request:

PIM-request

Fill in the justification and hit ‘Submit’:

PIM-request-justification

Approver

As an approver, start the approval of the PIM request by selecting the request and clicking ‘Approve’:

PIM-approve-request

Finally, check the request and if it meets requirements approve it with a justification:

PIM-approve-request-submit

Validate

Validate if the requesting user has an active role assignment under ‘My Roles’ in Privliged Identity Management:

validate-PIM-role-assignment

Conclusion

That concludes this blog about configuraing PIM via the ARM API with PowerShell. We have successfully:

✅ Created 2 new security groups. 1 for PIM requests, 1 for approval of requests
✅ Wrote a basic function to obtain headers that we used for making API calls
✅ Updated a role policy with a custom role settings
✅ Assigned the eligible role to our Security Group
✅ Successfully tested the workflow 🔐

With PIM now in place, your organization gains precise control over permissions, enhancing security and compliance. Hopefully this post provided you with some guidance on how to automate PIM in your environment. Good luck and feel free to reach out if you have any questions! 🚀