Showing posts with label AzureRBAC. Show all posts
Showing posts with label AzureRBAC. Show all posts

Wednesday, June 24, 2026

Automate Azure PIM Role Activation for Entra ID + Azure Resources with PowerShell

Automate Azure PIM Role Activation for Entra ID + Azure Resources with PowerShell

If you're working in a Zero Trust security environment, your Azure roles are likely managed through Privileged Identity Management (PIM) — meaning they expire and need to be manually re-activated every few hours. Clicking through the portal every time is tedious.

This PowerShell script automates activation for both Entra ID directory roles (via Microsoft Graph) and Azure resource roles (via ARM REST API) in a single run — with automatic detection of the maximum allowed duration from your PIM policy.


What This Script Does

  • Auto-installs all required PowerShell modules if not present
  • Activates all eligible Entra ID (Microsoft Graph) PIM roles — e.g. Global Reader, Security Administrator
  • Activates all eligible Azure Resource PIM roles — e.g. Contributor, Owner on subscriptions
  • Auto-detects the maximum allowed activation duration from your PIM policy per role
  • Displays a clean summary table of all eligible roles before activation

Prerequisites

  • PowerShell 5.1+ or PowerShell 7+
  • Internet access (modules are auto-installed if missing)
  • An account with eligible PIM role assignments in Entra ID or Azure subscriptions

The script will auto-install these modules if not found:

ModulePurpose
Az.AccountsAzure authentication & context
Az.ResourcesAzure AD user lookup
Microsoft.Graph.AuthenticationGraph API authentication
Microsoft.Graph.Identity.GovernancePIM role management
Microsoft.Graph.Identity.SignInsSign-in & identity operations

The Script

# ============================================================
#  Azure PIM Role Activator - Entra ID + Azure Resources
# ============================================================

# ── Install & Import Modules ─────────────────────────────────────────────────
foreach ($module in @("Az.Accounts", "Az.Resources")) {
    if (-not (Get-Module -ListAvailable -Name $module)) {
        Write-Host "Installing $module..." -ForegroundColor Yellow
        Install-Module $module -Scope CurrentUser -Force -AllowClobber
    }
    Import-Module $module -ErrorAction Stop
}

$targetVersion = (Get-Module -ListAvailable -Name Microsoft.Graph.Authentication | Sort-Object Version -Descending | Select-Object -First 1).Version
if (-not $targetVersion) {
    Install-Module Microsoft.Graph.Authentication -Scope CurrentUser -Force -AllowClobber
    $targetVersion = (Get-Module -ListAvailable -Name Microsoft.Graph.Authentication | Sort-Object Version -Descending | Select-Object -First 1).Version
}
foreach ($module in @("Microsoft.Graph.Authentication", "Microsoft.Graph.Identity.Governance", "Microsoft.Graph.Identity.SignIns")) {
    if (-not (Get-Module -ListAvailable -Name $module | Where-Object { $_.Version -eq $targetVersion })) {
        Write-Host "Installing $module $targetVersion..." -ForegroundColor Yellow
        Install-Module $module -RequiredVersion $targetVersion -Scope CurrentUser -Force -AllowClobber
    }
    Import-Module $module -RequiredVersion $targetVersion -ErrorAction Stop
}

# ============================================================
#  PART 1 — Entra ID (Microsoft Graph) PIM Roles
# ============================================================
Write-Host "`n===== ENTRA ID ROLES =====" -ForegroundColor Cyan

Disconnect-MgGraph -ErrorAction SilentlyContinue
Connect-MgGraph -Scopes "User.Read", "RoleManagement.ReadWrite.Directory", "RoleAssignmentSchedule.ReadWrite.Directory" -NoWelcome

$user = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/me" -Method GET
if (-not $user) { Write-Error "Failed to retrieve user."; exit }
Write-Host "Logged in as: $($user.userPrincipalName)" -ForegroundColor Green

$eligibleRoles = Get-MgRoleManagementDirectoryRoleEligibilitySchedule -Filter "principalId eq '$($user.id)'" -ExpandProperty RoleDefinition
if (-not $eligibleRoles) { Write-Warning "No eligible Entra ID PIM roles found." }

foreach ($role in $eligibleRoles) {
    try {
        $rawDuration = ((Get-MgPolicyRoleManagementPolicyAssignment `
            -Filter "scopeId eq '/' and scopeType eq 'DirectoryRole' and roleDefinitionId eq '$($role.RoleDefinitionId)'" `
            -ExpandProperty "Policy(`$expand=Rules)").Policy.Rules | Where-Object { $_.Id -eq "Expiration_EndUser_Assignment" }).AdditionalProperties["maximumDuration"]
        $hours = if ($rawDuration -match "PT(\d+)H") { [int]$Matches[1] } elseif ($rawDuration -match "P(\d+)D") { [int]$Matches[1]*24 } else { 8 }
        New-MgRoleManagementDirectoryRoleAssignmentScheduleRequest -BodyParameter @{
            Action           = "selfActivate"
            PrincipalId      = $user.id
            RoleDefinitionId = $role.RoleDefinitionId
            DirectoryScopeId = $role.DirectoryScopeId
            Justification    = "M365 Team"
            ScheduleInfo     = @{ StartDateTime = (Get-Date).ToUniversalTime(); Expiration = @{ Type = "AfterDuration"; Duration = "PT${hours}H" } }
        } | Out-Null
        Write-Host "  ✔ Activated : $($role.RoleDefinition.DisplayName) for $hours hr(s)" -ForegroundColor Green
    }
    catch { Write-Host "  ✘ Failed    : $($role.RoleDefinition.DisplayName) — $($_.Exception.Message)" -ForegroundColor Red }
}

# ============================================================
#  PART 2 — Azure Resources (ARM) PIM Roles
# ============================================================
Write-Host "`n===== AZURE RESOURCE ROLES =====" -ForegroundColor Cyan

Connect-AzAccount
$tokenObj = Get-AzAccessToken -ResourceUrl "https://management.azure.com"
$token    = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(
                [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($tokenObj.Token))
$headers  = @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" }
$userId   = (Get-AzADUser -SignedIn).Id

Write-Host "Subscription : $((Get-AzContext).Subscription.Name)" -ForegroundColor Green
Write-Host "User ID      : $userId" -ForegroundColor Green

$roles = (Invoke-RestMethod `
    -Uri "https://management.azure.com/providers/Microsoft.Authorization/roleEligibilityScheduleInstances?api-version=2020-10-01&`$filter=asTarget()" `
    -Headers $headers).value

if (-not $roles) { Write-Warning "No eligible Azure resource roles found." } else {
    $roles | ForEach-Object {
        [PSCustomObject]@{
            Role     = $_.properties.expandedProperties.roleDefinition.displayName
            Resource = $_.properties.expandedProperties.scope.displayName
            Type     = $_.properties.expandedProperties.scope.type
            Status   = $_.properties.status
        }
    } | Format-Table -AutoSize
}

Write-Host "`n===== ACTIVATING ALL ROLES =====" -ForegroundColor Yellow

foreach ($role in $roles) {

    $roleName  = $role.properties.expandedProperties.roleDefinition.displayName
    $scopePath = $role.properties.scope
    $roleDefId = $role.properties.roleDefinitionId

    # ── Auto-detect max allowed hours from PIM policy assignment ─────────────
    try {
        $policyAssignments = (Invoke-RestMethod `
            -Uri "https://management.azure.com$scopePath/providers/Microsoft.Authorization/roleManagementPolicyAssignments?api-version=2020-10-01&`$filter=roleDefinitionId eq '$roleDefId'" `
            -Headers $headers).value

        $policyId = $policyAssignments[0].properties.policyId

        $rules = (Invoke-RestMethod `
            -Uri "https://management.azure.com$policyId`?api-version=2020-10-01" `
            -Headers $headers).properties.effectiveRules

        $expiryRule  = $rules | Where-Object { $_.id -eq "Expiration_EndUser_Assignment" }
        $rawDuration = $expiryRule.maximumDuration

        $hours = if ($rawDuration -match "PT(\d+)H")     { [int]$Matches[1] }
                 elseif ($rawDuration -match "P(\d+)D")  { [int]$Matches[1] * 24 }
                 else                                     { 1 }

        Write-Host "  Policy max duration for '$roleName': $rawDuration ($hours hr)" -ForegroundColor DarkGray
    }
    catch {
        $hours = 1
        Write-Host "  Could not read policy for '$roleName', defaulting to 1hr" -ForegroundColor DarkYellow
    }

    # ── Activate the role ─────────────────────────────────────────────────────
    try {
        $body = @{ properties = @{
            principalId      = $userId
            roleDefinitionId = $roleDefId
            requestType      = "SelfActivate"
            linkedRoleEligibilityScheduleId = $role.properties.roleEligibilityScheduleId
            justification    = "Self activation"
            scheduleInfo     = @{ expiration = @{ type = "AfterDuration"; duration = "PT${hours}H" } }
        }} | ConvertTo-Json -Depth 10

        Invoke-RestMethod `
            -Uri "https://management.azure.com$scopePath/providers/Microsoft.Authorization/roleAssignmentScheduleRequests/$([guid]::NewGuid())?api-version=2020-10-01" `
            -Headers $headers -Method Put -Body $body | Out-Null

        Write-Host "  ✔ Activated : $roleName → $($role.properties.expandedProperties.scope.displayName) for $hours hour(s)" -ForegroundColor Green
    }
    catch {
        $err = ($_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue).error.message
        Write-Host "  ✘ Failed    : $roleName — $err" -ForegroundColor Red
    }
}

Write-Host "`nDone!" -ForegroundColor Cyan

How It Works

Module Auto-Install — The script checks for all required Az and Microsoft Graph modules and installs any missing ones before proceeding. It also pins Graph modules to the same version to avoid compatibility conflicts.

Part 1 — Entra ID Roles (Graph API) — Connects via Connect-MgGraph with the required scopes, fetches all eligible directory role assignments for the signed-in user, reads the max allowed duration from each role's PIM policy, and submits a selfActivate request via New-MgRoleManagementDirectoryRoleAssignmentScheduleRequest.

Part 2 — Azure Resource Roles (ARM REST API) — Connects via Connect-AzAccount, retrieves a bearer token (handling the SecureString conversion in newer Az module versions), queries the tenant-level PIM endpoint for eligible resource role assignments, auto-detects max duration from roleManagementPolicyAssignments, and submits activation requests via the ARM REST API.

Automate Azure PIM Role Activation with PowerShell

Automate Azure PIM Role Activation with PowerShell

Tired of manually clicking Activate in the Azure portal every time your PIM role expires?
This script automatically activates all eligible Azure resource roles — and even detects the maximum allowed duration from your PIM policy, so you never hit the "duration exceeds maximum" error again.


Prerequisites

  • Az PowerShell module installed (Install-Module Az -Scope CurrentUser -Force)
  • Eligible Azure RBAC roles assigned via PIM
  • Az.Accounts and Az.Resources modules

The Script

# ============================================================
#  Azure PIM Resource Role Activator - Full Script
# ============================================================

# ── Auto-install required modules ────────────────────────────────────────────
foreach ($module in @("Az.Accounts", "Az.Resources")) {
    if (-not (Get-Module -ListAvailable -Name $module)) {
        Write-Host "Installing $module..." -ForegroundColor Yellow
        Install-Module $module -Scope CurrentUser -Force -AllowClobber
    }
    Import-Module $module -ErrorAction Stop
}

# Or if you want to install the full Az bundle (all Az modules) instead:
# if (-not (Get-Module -ListAvailable -Name Az.Accounts)) {
#     Write-Host "Installing Az module..." -ForegroundColor Yellow
#     Install-Module Az -Scope CurrentUser -Force -AllowClobber
# }
# Import-Module Az.Accounts, Az.Resources -ErrorAction Stop

Connect-AzAccount

# Get token (handle SecureString in newer Az versions)
$tokenObj = Get-AzAccessToken -ResourceUrl "https://management.azure.com"
$token    = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(
                [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($tokenObj.Token))

$headers = @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" }
$subId   = (Get-AzContext).Subscription.Id
$userId  = (Get-AzADUser -SignedIn).Id

Write-Host "Subscription : $subId" -ForegroundColor Cyan
Write-Host "User ID      : $userId" -ForegroundColor Cyan

# ── Get eligible roles (tenant-level works, subscription-level doesn't) ──────
$roles = (Invoke-RestMethod `
    -Uri "https://management.azure.com/providers/Microsoft.Authorization/roleEligibilityScheduleInstances?api-version=2020-10-01&`$filter=asTarget()" `
    -Headers $headers).value
	
$roles | ForEach-Object {
    [PSCustomObject]@{
        Role         = $_.properties.expandedProperties.roleDefinition.displayName
        Resource     = $_.properties.expandedProperties.scope.displayName
        ResourceType = $_.properties.expandedProperties.scope.type
        Scope        = $_.properties.scope
        StartTime    = $_.properties.startDateTime
        EndTime      = $_.properties.endDateTime
        Status       = $_.properties.status
    }
} | Format-Table -AutoSize
	
Write-Host "`nFound $($roles.Count) eligible role(s)" -ForegroundColor Yellow
$roles | ForEach-Object {
    Write-Host "  • $($_.properties.expandedProperties.roleDefinition.displayName) → $($_.properties.expandedProperties.scope.displayName)"
}

Write-Host "`n===== ACTIVATING ALL ROLES =====" -ForegroundColor Yellow

foreach ($role in $roles) {

    $roleName  = $role.properties.expandedProperties.roleDefinition.displayName
    $scopePath = $role.properties.scope
    $roleDefId = $role.properties.roleDefinitionId

    # ── Auto-detect max allowed hours from PIM policy assignment ─────────────
    try {
        $policyAssignments = (Invoke-RestMethod `
            -Uri "https://management.azure.com$scopePath/providers/Microsoft.Authorization/roleManagementPolicyAssignments?api-version=2020-10-01&`$filter=roleDefinitionId eq '$roleDefId'" `
            -Headers $headers).value

        $policyId = $policyAssignments[0].properties.policyId

        $rules = (Invoke-RestMethod `
            -Uri "https://management.azure.com$policyId`?api-version=2020-10-01" `
            -Headers $headers).properties.effectiveRules

        $expiryRule  = $rules | Where-Object { $_.id -eq "Expiration_EndUser_Assignment" }
        $rawDuration = $expiryRule.maximumDuration

        $hours = if ($rawDuration -match "PT(\d+)H")     { [int]$Matches[1] }
                 elseif ($rawDuration -match "P(\d+)D")  { [int]$Matches[1] * 24 }
                 else                                     { 1 }

        Write-Host "  Policy max duration for '$roleName': $rawDuration ($hours hr)" -ForegroundColor DarkGray
    }
    catch {
        $hours = 1
        Write-Host "  Could not read policy for '$roleName', defaulting to 1hr" -ForegroundColor DarkYellow
    }

    # ── Activate the role ─────────────────────────────────────────────────────
    try {
        $body = @{ properties = @{
            principalId      = $userId
            roleDefinitionId = $roleDefId
            requestType      = "SelfActivate"
            linkedRoleEligibilityScheduleId = $role.properties.roleEligibilityScheduleId
            justification    = "Self activation"
            scheduleInfo     = @{ expiration = @{ type = "AfterDuration"; duration = "PT${hours}H" } }
        }} | ConvertTo-Json -Depth 10

        Invoke-RestMethod `
            -Uri "https://management.azure.com$scopePath/providers/Microsoft.Authorization/roleAssignmentScheduleRequests/$([guid]::NewGuid())?api-version=2020-10-01" `
            -Headers $headers -Method Put -Body $body | Out-Null

        Write-Host "  ✔ Activated : $roleName → $($role.properties.expandedProperties.scope.displayName) for $hours hour(s)" -ForegroundColor Green
    }
    catch {
        $err = ($_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue).error.message
        Write-Host "  ✘ Failed    : $roleName — $err" -ForegroundColor Red
    }
}

Write-Host "`nDone!" -ForegroundColor Cyan

How It Works

1. Authentication — Connects to Azure and retrieves a bearer token, handling the SecureString conversion introduced in newer Az module versions.

2. Eligible Role Discovery — Queries the tenant-level PIM endpoint to fetch all eligible role assignments for the signed-in user.

3. Auto-Duration Detection — For each role, it reads the roleManagementPolicyAssignment to find the Expiration_EndUser_Assignment rule and extracts the maximumDuration (e.g. PT4H). This prevents the "duration exceeds maximum" API error.

4. Role Activation — Submits a SelfActivate request for each eligible role using the exact maximum duration allowed by policy.



Featured Post

Power Automate Desktop: Find One-to-Many Values Between Two Columns

Power Automate Desktop: Find One-to-Many Values Between Two Columns The Problem You have a table with two columns, A and B . You want to fi...

Popular posts