Privileged Identity Management (PIM) is a critical security service within Microsoft Entra ID that provides just-in-time (JIT) privileged access to resources. This article explores PIM as a modern privileged identity management tool, the often-overlooked importance of governance over Entra Id PIM Approvers and technical implementation details of this technology in the Microsoft Graph API.
What is PIM and where does it sit in your identity management landscape?
Securing your Azure Workforce Tenant
Privileged Identity Management serves as a robust security control mechanism by implementing the principle of least privilege across your Entra ID environment. PIM accomplishes this by:
- Converting permanent privileges into temporary (eligible) assignments on need by need basis
- Enabling the ability for authentication challenges before roles can be activated
- Enforcing time-bound access with configurable activation durations
- Creating comprehensive audit trails for all privileged escalations
- Supporting approval workflows for sensitive role activations like Global Admin
Why Organizations Implement PIM
Organizations implement PIM to mitigate the risks associated with standing or stale privileges. Standing privileges represent a significant security vulnerability, as compromised privileged accounts provide attackers with persistent access to critical systems and allow for token theft to enable privileged controls. By implementing just-in-time access, PIM reduces the attack surface by:
- Minimizing the duration of activated privileged roles
- Establishing approval gates for sensitive role activations
- Creating comprehensive logs of who requested access, who approved it, and what actions were performed
Enforcing multi-factor authentication for privileged role activation
Importantly, it should be noted that PIM doesn't mean you should assign Global Admin eligibility to standard productivity accounts (e.g. accounts with mailboxes). Segregation of Admin accounts and standard productivity accounts is still an important practice which mitigates Spear Phishing and common token theft exploits.
The Critical Role of Approvers in PIM
While User Access Reviews (UAR) are a common practice for reviewing role assignments in Entra ID, less attention is typically paid to who can approve role activations. This oversight creates a significant security gap. Approvers wield substantial power within your PIM environment. They determine who can activate privileged roles and when those activations can occur. They also can be the weak link when socially engineered. Therefore, you must ensure that your approvers for highly privileged roles are minimized and given the proper training to be aware of BEC scams.
Key considerations regarding approvers include:
- Ensuring approvers hold positions with appropriate authority to grant privileged access
- Preventing self-approval scenarios where users can approve their own role activations
- Avoiding situations where an approver can approve elevated permissions for their own administrative accounts
- Implementing regular reviews of approver assignments to prevent privilege creep
- Ensure approvers are given ample training to identify spear phishing attacks
The Importance of Access Reviews When Using PIM
Access reviews are essential for maintaining the security posture of your PIM implementation:
- Regular Role Assignment Reviews: Ensure only authorized individuals retain eligibility for privileged roles
- Approver Reviews: Validate that approvers maintain appropriate authority and don't create conflict-of-interest scenarios
- Activation Pattern Analysis: Identify unusual activation patterns that might indicate compromise or abuse. E.g. no corresponding ticket to the level of privilege
- Policy Configuration Validation: Confirm activation settings (duration, MFA requirements) align with security policies
How to review PIM Approvers in Microsoft Entra ID
Reviewing approvers for Directory and Azure Roles is not an easy activity in the Microsoft Entra portal. You will need to individually check each Directory Role configuration's direct approvers and indirect approvers (via groups) individually. Therefore, the easiest way is to export this into a CSV report to line by line verify each individual user.
How PIM Works Under the Hood in Microsoft Graph API
Understanding PIM's implementation in Microsoft Graph API provides insights into how this data is stored and can be retrieved for review.
PIM Policy and Rule Structure
PIM's architecture in Microsoft Graph is composed of several key components:
- Directory Roles: The Entra ID roles that can be assigned (e.g., Global Administrator, User Administrator)
- Policies: Unified Role Management Policies that define how roles can be activated
- Policy Rules: Specific controls within policies that govern different aspects of role activation. These can include approvals, duration, required activation information and who is notified of an activation.
- Assignments / Schedules: Role eligibility assignments link users/groups to roles with specified policies
This structure allows for granular control over who can activate what roles and under what conditions. However, it can be hard to extract this information from Microsoft Graph if you don't understand the roles of each object.
How Approvers Are Stored in PIM
Based on the above analysis we can infer that the Approvers will be stored in a Policy Rule which is associated with a Policy and then a Directory Role. Each Policy Rule has a static well-known name. In the case of approvers is called Approval_EndUser_Assignment.
End User Approval Assignment Policy Rule
This rule has a few configurations that are not present in the UI. The following is worth noting for special use cases via PIM:
- Can specify both primary approvers and escalation approvers
- Supports both direct user assignments and group-based assignments
- Contains settings for approval thresholds and justification requirements
Extracting the PIM Approvers via Graph API / PowerShell
To effectively audit your PIM approver structure, you'll need to:
- Retrieve all role assignments using /roleManagement/directory/roleEligibilitySchedules
- For each assignment, find the associated policy via its policyId
- Within each policy, locate the Approval_EndUser_Assignment rule
- Enumerate both primary and escalation approvers from the rule
- For group-based approvers, further enumerate all group members
This approach provides comprehensive visibility into who can approve role activations throughout your organization.
In essence you would look to have the following script:
You can find a full export module here: https://github.com/Modern42/Blogs/blob/main/entra-id/pim/Get-PIMApprovers.psm1
# Module: Get-PIMApprovers.psm1
# Requires -Modules Microsoft.Graph.Identity.Governance, Microsoft.Graph.Groups, Microsoft.Graph.Users, Microsoft.Graph.DirectoryObjects
# Requires -Version 5.1
function Get-PIMApprovers {
[CmdletBinding()]
param (
[Parameter()]
[string]$OutputPath = ".\PIMApprovers.csv",
[Parameter()]
[switch]$IncludeInactiveRoles
)
begin {
try {
# Check if already connected to Microsoft Graph
$context = Get-MgContext
if (-not $context) {
Write-Error "Not connected to Microsoft Graph. Please run Connect-MgGraph with Directory.Read.All and PrivilegedAccess.Read.AzureAD permissions"
return
}
# Initialize results array
$results = @()
# Create a hashtable to store role definitions
$roleDefinitions = @{}
}
catch {
Write-Error "Error in initialization: $_"
return
}
}
process {
try {
# Get all role definitions in one call
Write-Verbose "Fetching all role definitions..."
$allRoleDefinitions = Get-MgDirectoryRoleTemplate
foreach ($def in $allRoleDefinitions) {
$roleDefinitions[$def.Id] = $def
}
# Get all policy assignments and their rules in one call
Write-Verbose "Fetching policy assignments and rules..."
$policyAssignments = Get-MgPolicyRoleManagementPolicyAssignment -Filter "scopeId eq '/' and scopeType eq 'Directory'" -ExpandProperty "policy(`$expand=rules)"
# Setup progress bar
$totalAssignments = $policyAssignments.Count
$currentAssignment = 0
foreach ($assignment in $policyAssignments) {
# Update progress bar
$currentAssignment++
$percentComplete = [math]::Round(($currentAssignment / $totalAssignments) * 100)
$roleName = $roleDefinitions[$assignment.RoleDefinitionId].DisplayName
Write-Progress -Activity "Processing PIM Role Approvers" -Status "Role $currentAssignment of $totalAssignments" `
-PercentComplete $percentComplete -CurrentOperation $roleName
Write-Verbose "Processing assignment for role template: $($assignment.RoleDefinitionId)"
# Get role definition from our hashtable
$roleDefinition = $roleDefinitions[$assignment.RoleDefinitionId]
if (-not $roleDefinition) {
Write-Warning "Could not find role definition for template ID: $($assignment.RoleDefinitionId)"
continue
}
# Get approval settings from policy rules
$approvalRule = $assignment.Policy.Rules | Where-Object { $_.Id -eq 'Approval_EndUser_Assignment' -and $_.AdditionalProperties.setting.isApprovalRequired -eq $true }
if (-not $approvalRule) {
Write-Verbose "No approval settings found / or is required for role: $($roleDefinition.DisplayName)"
continue
}
# https://learn.microsoft.com/en-us/graph/api/resources/unifiedrolemanagementpolicyapprovalrule?view=graph-rest-1.0
# Process primary approvers
$primaryApprovers = $approvalRule.AdditionalProperties.setting.approvalStages.primaryApprovers
if ($primaryApprovers) {
foreach ($approver in $primaryApprovers) {
if ($approver["@odata.type"] -eq '#microsoft.graph.singleUser') {
# Get user details
$approverDetails = Get-MgUser -UserId $approver.userId -Property "displayName,id,userPrincipalName,accountEnabled"
if ($approverDetails) {
$results += [PSCustomObject]@{
RoleName = $roleDefinition.DisplayName
RoleDescription = $roleDefinition.Description
RoleTemplateId = $assignment.RoleDefinitionId
ApproverId = $approverDetails.Id
ApproverName = $approverDetails.DisplayName
ApproverUPN = $approverDetails.UserPrincipalName
ApproverType = 'Direct'
ApprovalStep = 'Primary'
ApproverEnabled = $approverDetails.AccountEnabled
GroupId = ''
GroupName = ''
PolicyId = $assignment.PolicyId
RequiresApproval = $true
ApproverCount = $approvalRule.AdditionalProperties.setting.approvalRequired
ApprovalDuration = $approvalRule.AdditionalProperties.setting.durationInDays
}
}
}
elseif ($approver["@odata.type"] -eq '#microsoft.graph.groupMembers') {
# Get group members
$groupMembers = Get-MgGroupMemberAsUser -GroupId $approver.groupId -Property "displayName,id,userPrincipalName,accountEnabled" -All
foreach ($member in $groupMembers) {
$results += [PSCustomObject]@{
RoleName = $roleDefinition.DisplayName
RoleDescription = $roleDefinition.Description
RoleTemplateId = $assignment.RoleDefinitionId
ApproverId = $member.Id
ApproverName = $member.DisplayName
ApproverUPN = $member.UserPrincipalName
ApproverType = 'Group Member'
ApprovalStep = 'Primary'
ApproverEnabled = $member.AccountEnabled
GroupId = $approver.groupId
GroupName = $approver.description
PolicyId = $assignment.PolicyId
RequiresApproval = $true
ApproverCount = $approvalRule.AdditionalProperties.setting.approvalRequired
ApprovalDuration = $approvalRule.AdditionalProperties.setting.durationInDays
}
}
}
}
}
# Process escalation approvers
$escalationApprovers = $approvalRule.AdditionalProperties.setting.approvalStages.escalationApprovers
if ($escalationApprovers) {
foreach ($approver in $escalationApprovers) {
if ($approver["@odata.type"] -eq '#microsoft.graph.singleUser') {
# Get user details
$approverDetails = Get-MgUser -UserId $approver.userId -Property "displayName,id,userPrincipalName,accountEnabled"
if ($approverDetails) {
$results += [PSCustomObject]@{
RoleName = $roleDefinition.DisplayName
RoleDescription = $roleDefinition.Description
RoleTemplateId = $assignment.RoleDefinitionId
ApproverId = $approverDetails.Id
ApproverName = $approverDetails.DisplayName
ApproverUPN = $approverDetails.UserPrincipalName
ApproverType = 'Direct'
ApprovalStep = 'Escalation'
ApproverEnabled = $approverDetails.AccountEnabled
GroupId = ''
GroupName = ''
PolicyId = $assignment.PolicyId
RequiresApproval = $true
ApproverCount = $approvalRule.AdditionalProperties.setting.approvalRequired
ApprovalDuration = $approvalRule.AdditionalProperties.setting.durationInDays
EscalationTime = $approvalRule.AdditionalProperties.setting.approvalStages.escalationTimeInMinutes
}
}
}
elseif ($approver["@odata.type"] -eq '#microsoft.graph.groupMembers') {
# Get group members
$groupMembers = Get-MgGroupMemberAsUser -GroupId $approver.groupId -Property "displayName,id,userPrincipalName,accountEnabled" -All
foreach ($member in $groupMembers) {
$results += [PSCustomObject]@{
RoleName = $roleDefinition.DisplayName
RoleDescription = $roleDefinition.Description
RoleTemplateId = $assignment.RoleDefinitionId
ApproverId = $member.Id
ApproverName = $member.DisplayName
ApproverUPN = $member.UserPrincipalName
ApproverType = 'Group Member'
ApprovalStep = 'Escalation'
ApproverEnabled = $member.AccountEnabled
GroupId = $approver.groupId
GroupName = $approver.description
PolicyId = $assignment.PolicyId
RequiresApproval = $true
ApproverCount = $approvalRule.AdditionalProperties.setting.approvalRequired
ApprovalDuration = $approvalRule.AdditionalProperties.setting.durationInDays
EscalationTime = $approvalRule.AdditionalProperties.setting.approvalStages.escalationTimeInMinutes
}
}
}
}
}
}
# Clear progress bar
Write-Progress -Activity "Processing PIM Role Approvers" -Completed
# Export results to CSV
if ($results.Count -gt 0) {
$results | Export-Csv -Path $OutputPath -NoTypeInformation
Write-Host "Exported $($results.Count) PIM approvers to $OutputPath"
}
else {
Write-Warning "No PIM approvers found to export"
}
}
catch {
Write-Error "Error processing roles: $_"
Write-Error $_.Exception.StackTrace
}
}
end {
try {
# Return the results object for pipeline use
return $results
}
catch {
Write-Error "Error in cleanup: $_"
}
}
}
Export-ModuleMember -Function Get-PIMApproversThe key to making these API calls efficient is to the use the OData expand feature on the unifiedRoleManagementPolicyAssignment to retrieve its Policy Rules relationship. This returns each policy that is scoped to Entra Id Directory Roles and the associated rules. We can then iterate each policy, find the approver's rule, extract the assignments and export the data. Importantly, policies do not have any information about the directory role, only the Id. So prior to running this command, it is advantageous to create a hash map of all directory roles by their Ids.
Conclusion
Privileged Identity Management provides robust security controls for Entra ID environments, but effective implementation requires diligent governance -- particularly regarding eligibility assignments and the approvers. By understanding PIM's underlying architecture and implementing the recommended best practices, organizations can significantly reduce the risk of privilege escalation while maintaining operational efficiency.
Regular reviews of both role assignments and approver configurations are essential components of a comprehensive PIM governance strategy. By treating approvers with the same scrutiny as privileged role assignments, organizations can close a commonly overlooked security gap in their identity management infrastructure. If your organisation needs help taking the next steps with PIM, reach out and see if we are the right fit for you.




