Invoke-PIMActivator.ps1

<#PSScriptInfo
.VERSION 1.0.0
.GUID a2b49d31-ec1a-43d0-97ef-401a9bed8d16
.NAME Invoke-PIMActivator
.AUTHOR Josh (uniQuk)
.COMPANYNAME
.COPYRIGHT
.TAGS Azure Entra PIM PrivilegedIdentityManagement Graph Microsoft365 Security
.LICENSEURI https://github.com/uniQuk/GraphPS/tree/main?tab=MIT-1-ov-file#MIT-1-ov-file
.PROJECTURI https://github.com/uniQuk/GraphPS
.ICONURI
.EXTERNALMODULEDEPENDENCIES Microsoft.Graph.Authentication
.REQUIREDSCRIPTS
.EXTERNALSCRIPTDEPENDENCIES
.RELEASENOTES
  v1.0.0 - Initial release of Invoke-PIMActivator
#>


#-----------------------------------------------------------
# Ensure you're connected to Microsoft Graph with the required scopes.
# Author: Josh (https://github.com/uniQuk)
#-----------------------------------------------------------

<#
.SYNOPSIS
    Activates and manages Microsoft Entra Privileged Identity Management (PIM) roles and groups.
 
.DESCRIPTION
Activates and manages Microsoft Entra Privileged Identity Management (PIM) roles and groups.
 
.NOTES
    Required Permissions:
    - PrivilegedEligibilitySchedule.Read.AzureADGroup : Required to view eligible privileged groups
    - PrivilegedAssignmentSchedule.ReadWrite.AzureADGroup : Required to activate/deactivate group memberships
    - RoleEligibilitySchedule.Read.Directory : Required to view eligible directory roles
    - RoleAssignmentSchedule.ReadWrite.Directory : Required to activate/deactivate directory roles
     
    To connect with these permissions, run:
    Connect-MgGraph -Scopes "PrivilegedEligibilitySchedule.Read.AzureADGroup","PrivilegedAssignmentSchedule.ReadWrite.AzureADGroup","RoleEligibilitySchedule.Read.Directory","RoleAssignmentSchedule.ReadWrite.Directory"
 
.EXAMPLE
    # First connect to Microsoft Graph with required permissions
    Connect-MgGraph -Scopes "PrivilegedEligibilitySchedule.Read.AzureADGroup","PrivilegedAssignmentSchedule.ReadWrite.AzureADGroup","RoleEligibilitySchedule.Read.Directory","RoleAssignmentSchedule.ReadWrite.Directory"
     
    # Run the script
    .\Graph-PIMActivator.ps1
#>


#-----------------------------------------------------------
# Check Connection to Microsoft Graph First
#-----------------------------------------------------------
function Test-GraphConnection {
    [CmdletBinding()]
    param(
        [switch]$ShowDetails
    )

    # This set matches our testing results - the minimum required permissions
    $requiredScopes = @(
        # For PIM Group operations
        "PrivilegedEligibilitySchedule.Read.AzureADGroup",
        "PrivilegedAssignmentSchedule.ReadWrite.AzureADGroup",
        
        # For PIM Role operations
        "RoleEligibilitySchedule.Read.Directory",
        "RoleAssignmentSchedule.ReadWrite.Directory"
    )
    
    # Check if connected to Microsoft Graph
    try {
        $context = Get-MgContext -ErrorAction Stop
        if (-not $context) {
            Write-Host "`n[ERROR] Not connected to Microsoft Graph. Please connect first with:" -ForegroundColor Red
            Write-Host "Connect-MgGraph -Scopes '$($requiredScopes -join "','")'" -ForegroundColor Yellow
            return $false
        }
    }
    catch {
        Write-Host "`n[ERROR] Not connected to Microsoft Graph. Please connect first with:" -ForegroundColor Red
        Write-Host "Connect-MgGraph -Scopes '$($requiredScopes -join "','")'" -ForegroundColor Yellow
        return $false
    }

    # Get current scopes
    $currentScopes = $context.Scopes

    # Check for required permissions
    $missingScopes = @()
    foreach ($scope in $requiredScopes) {
        if ($currentScopes -notcontains $scope) {
            $missingScopes += $scope
        }
    }

    # Print connection information
    Write-Host "`n=== Microsoft Graph Connection ===" -ForegroundColor Cyan
    Write-Host "Connected as: $($context.Account)" -ForegroundColor White
    Write-Host "Tenant: $($context.TenantId)" -ForegroundColor White
    
    if ($ShowDetails) {
        Write-Host "Environment: $($context.Environment)" -ForegroundColor White
        Write-Host "App: $($context.AppName) ($($context.ClientId))" -ForegroundColor White
        Write-Host "Authentication: $($context.AuthType)" -ForegroundColor White
    }

    # Permission Analysis
    if ($missingScopes.Count -gt 0) {
        Write-Host "`n[WARNING] Missing required permissions: " -ForegroundColor Red
        $missingScopes | ForEach-Object { Write-Host "- $_" -ForegroundColor Red }
        
        # Check for alternative permissions that might work
        $alternatives = @(
            "Directory.AccessAsUser.All", 
            "RoleManagement.ReadWrite.Directory",
            "PrivilegedAccess.ReadWrite.AzureADGroup"
        )
        
        $hasAlternatives = $false
        foreach ($alt in $alternatives) {
            if ($currentScopes -contains $alt) {
                $hasAlternatives = $true
                break
            }
        }
        
        if ($hasAlternatives) {
            Write-Host "`nYou have some alternative permissions that might work." -ForegroundColor Yellow
            return $true # Continue with warning
        } else {
            Write-Host "`nPlease reconnect with all required permissions:" -ForegroundColor Yellow
            Write-Host "Disconnect-MgGraph" -ForegroundColor Yellow
            Write-Host "Connect-MgGraph -Scopes '$($requiredScopes -join "','")'" -ForegroundColor Yellow
            return $false
        }
    } else {
        Write-Host "`n✓ Connected with all required permissions" -ForegroundColor Green
    }
    
    return $true
}

# Check connection before proceeding - IMPORTANT - Add this BEFORE any API calls
if (-not (Test-GraphConnection)) {
    Write-Host "`nExiting script. Please connect with required permissions and try again." -ForegroundColor Red
    exit
}

#-----------------------------------------------------------
# Helper Functions
#-----------------------------------------------------------

# Get the display name for a group using its group ID.
function Get-GroupName {
    param ([string]$GroupId)
    try {
        (Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/groups/$GroupId").displayName
    }
    catch {
        return $GroupId
    }
}

# Extract error details from Graph API response.
function Get-ErrorDetails {
    param (
        [Parameter(Mandatory=$true)]
        [System.Management.Automation.ErrorRecord]$ErrorRecord
    )
    $errorDetails = @{ Message = $ErrorRecord.Exception.Message; Details = "No additional details" }
    try {
        if ($ErrorRecord.ErrorDetails -and $ErrorRecord.ErrorDetails.Message) {
            $errorJson = $ErrorRecord.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue
            if ($errorJson.error) {
                $errorDetails.Details = "Code: $($errorJson.error.code)`nMessage: $($errorJson.error.message)"
                if ($errorJson.error.details) {
                    $errorDetails.Details += "`nAdditional details:"
                    foreach ($detail in $errorJson.error.details) {
                        $errorDetails.Details += "`n- $($detail.target): $($detail.message)"
                    }
                }
            }
        }
    }
    catch { }
    return $errorDetails
}

# Check if an assignment is locked (activated less than 5 minutes ago).
function Test-AssignmentLock {
    param ($item)
    $baseTime = if ($item.PSObject.Properties['createdDateTime']) { 
                    [datetime]$item.createdDateTime 
                } else { 
                    [datetime]$item.startDateTime 
                }
    return ((Get-Date) -lt $baseTime.AddMinutes(5))
}

# Wait for deactivation to complete.
function Wait-ForDeactivation {
    param (
        [string]$Type,      # "Role" or "Group"
        [string]$Id,        # roleDefinitionId or groupId
        [int]$Timeout = 30,
        [int]$Interval = 5
    )
    $elapsed = 0
    while ($elapsed -lt $Timeout) {
        if ($Type -eq "Group") {
            $active = (Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/identityGovernance/privilegedAccess/group/assignmentScheduleInstances/filterByCurrentUser(on='principal')").value |
                      Where-Object { $_.groupId -eq $Id }
        }
        else {
            $active = (Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignmentScheduleInstances/filterByCurrentUser(on='principal')").value |
                      Where-Object { $_.roleDefinitionId -eq $Id }
        }
        if (-not $active) { return $true }
        Write-Host "Waiting for deactivation to complete... ($elapsed seconds elapsed)" -ForegroundColor Yellow
        Start-Sleep -Seconds $Interval
        $elapsed += $Interval
    }
    return $false
}

# Validate payload with reprompt logic.
function Test-Payload {
    param (
        [hashtable]$Payload,
        [string]$Endpoint,
        [int]$AttemptsLimit = 3
    )
    $attempts = 0
    while ($attempts -lt $AttemptsLimit) {
        try {
            Write-Host "Validating request at $Endpoint..." -ForegroundColor Cyan
            $jsonPayload = $Payload | ConvertTo-Json -Depth 5
            Invoke-MgGraphRequest -Method POST -Uri $Endpoint -Body $jsonPayload -ContentType "application/json" | Out-Null
            Write-Host "Validation succeeded." -ForegroundColor Green
            return $Payload
        }
        catch {
            $attempts++
            $errorDetails = Get-ErrorDetails -ErrorRecord $_
            Write-Host "Validation failed: $($errorDetails.Message)" -ForegroundColor Red
            Write-Host $errorDetails.Details -ForegroundColor Red
            $errorMsg = $errorDetails.Message + " " + $errorDetails.Details
            if ($errorMsg -match "justification|Justification|reason") {
                $Payload.justification = Read-Host "Enter updated justification"
            }
            elseif ($errorMsg -match "ticket|Ticket|reference") {
                $Payload.ticketInfo = @{ "ticketNumber" = (Read-Host "Enter updated ticket info") }
            }
            else {
                Write-Host "Validation error encountered." -ForegroundColor Yellow
                $Payload.justification = Read-Host "Enter updated justification (if required)"
                $ticket = Read-Host "Enter ticket number (if required)"
                if ($ticket) { $Payload.ticketInfo = @{ "ticketNumber" = $ticket } }
            }
        }
    }
    throw "Too many validation attempts. Exiting."
}

# Process an assignment into a uniform object.
function ConvertTo-AssignmentObject {
    param (
        [object]$Assignment,
        [string]$Type,   # "Role" or "Group"
        [string]$State   # "Active" or "Eligible"
    )
    # For active assignments, skip if required properties are missing.
    if ($State -eq "Active") {
        if ($Type -eq "Role" -and (-not $Assignment.roleAssignmentScheduleId -or -not $Assignment.endDateTime)) { return $null }
        if ($Type -eq "Group" -and (-not $Assignment.assignmentScheduleId -or -not $Assignment.endDateTime)) { return $null }
    }
    $obj = [PSCustomObject]@{
        Type             = $Type
        State            = $State
        Name             = if ($Type -eq "Role") { $Assignment.roleDefinition.displayName } else { Get-GroupName $Assignment.groupId }
        RoleDefinitionId = if ($Type -eq "Role") { $Assignment.roleDefinitionId } else { $null }
        GroupId          = if ($Type -eq "Group") { $Assignment.groupId } else { $null }
        StartDateTime    = $Assignment.startDateTime
        EndDateTime      = $Assignment.endDateTime
        Raw              = $Assignment
        Locked           = $false
    }
    if ($State -eq "Active") { $obj.Locked = Test-AssignmentLock $Assignment }
    return $obj
}

#-----------------------------------------------------------
# Main Script
#-----------------------------------------------------------

# Query current user.
$currentUser = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/me"
$userId = $currentUser.id
Write-Host "Getting PIM roles and groups for user: $($currentUser.displayName) ($userId)" -ForegroundColor White
Write-Host ""

# Retrieve assignments.
$activeRoles    = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignmentScheduleInstances?`$filter=principalId eq '$userId'&`$expand=roleDefinition"
$eligibleRoles  = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/roleManagement/directory/roleEligibilityScheduleInstances?`$filter=principalId eq '$userId'&`$expand=roleDefinition"
$activeGroups   = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/identityGovernance/privilegedAccess/group/assignmentScheduleInstances/filterByCurrentUser(on='principal')"
$eligibleGroups = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/identityGovernance/privilegedAccess/group/eligibilitySchedules/filterByCurrentUser(on='principal')"

# Process assignments.
$processedActiveRoles    = @($activeRoles.value | ForEach-Object { ConvertTo-AssignmentObject -Assignment $_ -Type "Role" -State "Active" } | Where-Object { $_ })
$processedActiveGroups   = @($activeGroups.value | ForEach-Object { ConvertTo-AssignmentObject -Assignment $_ -Type "Group" -State "Active" } | Where-Object { $_ })
$processedEligibleRoles  = @($eligibleRoles.value | ForEach-Object { ConvertTo-AssignmentObject -Assignment $_ -Type "Role" -State "Eligible" } | Where-Object { $_ })
$processedEligibleGroups = @($eligibleGroups.value | ForEach-Object { ConvertTo-AssignmentObject -Assignment $_ -Type "Group" -State "Eligible" } | Where-Object { $_ })

# Filter out eligible assignments that already have an active counterpart.
$filteredEligibleRoles = @($processedEligibleRoles | Where-Object {
    $eligible = $_
    $activeMatch = @($processedActiveRoles | Where-Object { $_.RoleDefinitionId -eq $eligible.RoleDefinitionId })
    ($activeMatch.Count -eq 0)
})

$filteredEligibleGroups = @($processedEligibleGroups | Where-Object {
    $eligible = $_
    $activeMatch = @($processedActiveGroups | Where-Object { $_.GroupId -eq $eligible.GroupId })
    ($activeMatch.Count -eq 0)
})

# Combine assignments for display safely
$displayItems = @()
if ($processedActiveRoles.Count -gt 0) { $displayItems += $processedActiveRoles }
if ($processedActiveGroups.Count -gt 0) { $displayItems += $processedActiveGroups }
if ($filteredEligibleRoles.Count -gt 0) { $displayItems += $filteredEligibleRoles }
if ($filteredEligibleGroups.Count -gt 0) { $displayItems += $filteredEligibleGroups }

Write-Host "`n=== All PIM Roles and Groups ===" -ForegroundColor Cyan
Write-Host "Note: Recently activated roles require a 5-minute waiting period before they can be modified." -ForegroundColor Yellow

$headerFormat = "{0,-6} {1,-8} {2,-35} {3,-19} {4,-19} {5,-10} {6,-8}"
$header = $headerFormat -f "Type", "State", "Name", "Start Time", "End Time", "Status", "Action"
Write-Host $header -ForegroundColor Green
Write-Host ("-" * 110) -ForegroundColor Green

# Build menu items.
$menuItems = @()
$menuIndex = 1

foreach ($item in $displayItems) {
    $startStr = if ($item.StartDateTime) { ([datetime]$item.StartDateTime).ToString("yyyy-MM-dd HH:mm") } else { "N/A" }
    $endStr   = if ($item.EndDateTime)   { ([datetime]$item.EndDateTime).ToString("yyyy-MM-dd HH:mm") }   else { "Permanent" }
    $availability = if ($item.Locked) { "Locked" } else { "Available" }
    $ready = if ($item.Locked) {
        $baseTime = if ($item.Raw.PSObject.Properties['createdDateTime']) { [datetime]$item.Raw.createdDateTime } else { [datetime]$item.StartDateTime }
        $timeLeft = [math]::Ceiling(($baseTime.AddMinutes(5) - (Get-Date)).TotalMinutes)
        "Wait ${timeLeft}m"
    }
    else { "Ready" }
    Write-Host ($headerFormat -f $item.Type, $item.State, $item.Name.Substring(0, [Math]::Min(35, $item.Name.Length)), $startStr, $endStr, $availability, $ready)
    if (-not $item.Locked) {
        $item | Add-Member -NotePropertyName MenuIndex -NotePropertyValue $menuIndex -Force
        $menuItems += $item
        $menuIndex++
    }
}

Write-Host "`n=== PIM Activation/Modification Menu ===" -ForegroundColor Cyan
$menuItems | ForEach-Object { Write-Host "$($_.MenuIndex). [$($_.State) $($_.Type)] $($_.Name)" -ForegroundColor White }
Write-Host "0. Exit without modifying" -ForegroundColor Cyan

$selection = Read-Host "Select an assignment to modify (0-$($menuItems.Count))"
if ($selection -eq "0") { Write-Host "Exiting." -ForegroundColor Yellow; exit }
$selectedItem = $menuItems | Where-Object { $_.MenuIndex -eq [int]$selection }
if (-not $selectedItem) { Write-Host "Invalid selection." -ForegroundColor Yellow; exit }
if ($selectedItem.Locked) { Write-Host "Selected assignment is locked. Cannot modify." -ForegroundColor Red; exit }
Write-Host "You selected [$($selectedItem.State) $($selectedItem.Type)] '$($selectedItem.Name)'." -ForegroundColor White

#-----------------------------------------------------------
# Determine Action Based on Assignment State
#-----------------------------------------------------------
if ($selectedItem.State -eq "Eligible") {
    $action = "selfActivate"
    Write-Host "Action: Activate eligible assignment." -ForegroundColor Green
}
elseif ($selectedItem.State -eq "Active") {
    $choice = Read-Host "Assignment is active. Would you like to Extend (E) or Deactivate (D)? (E/D)"
    if ($choice -match "^[Ee]") {
         if (Test-AssignmentLock $selectedItem.Raw) { Write-Host "Cannot extend: Less than 5 minutes since activation." -ForegroundColor Red; exit }
         $action = "extend"
         Write-Host "Action: Extend active assignment." -ForegroundColor Green
    }
    elseif ($choice -match "^[Dd]") {
         if (Test-AssignmentLock $selectedItem.Raw) { Write-Host "Cannot deactivate: Less than 5 minutes since activation." -ForegroundColor Red; exit }
         $action = "selfDeactivate"
         Write-Host "Action: Deactivate active assignment." -ForegroundColor Green
    }
    else { Write-Host "Invalid choice. Exiting." -ForegroundColor Yellow; exit }
}
else { Write-Host "Unknown assignment state." -ForegroundColor Red; exit }

#-----------------------------------------------------------
# Prompt for Duration and Build ScheduleInfo (if needed)
#-----------------------------------------------------------
if ($action -in @("selfActivate", "extend")) {
    $defaultDuration = 8
    if ($selectedItem.EndDateTime -and $selectedItem.StartDateTime) {
         $defaultDuration = [math]::Round((([datetime]$selectedItem.EndDateTime - [datetime]$selectedItem.StartDateTime).TotalHours),2)
    }
    $durationInput = Read-Host "Enter duration in hours (default: $defaultDuration)"
    $duration = if ([string]::IsNullOrEmpty($durationInput)) { $defaultDuration } else { [double]$durationInput }
    if ($duration -eq [math]::Floor($duration)) { 
         $durationIso = "PT$($duration)H" 
    } else { 
         $minutes = [math]::Round($duration * 60)
         $durationIso = "PT$($minutes)M" 
    }
    $scheduleInfo = @{ 
        "startDateTime" = (Get-Date).ToUniversalTime().ToString("o")
        "expiration"    = @{ "type" = "afterDuration"; "duration" = $durationIso }
    }
} else {
    $scheduleInfo = @{ "startDateTime" = (Get-Date).ToUniversalTime().ToString("o") }
}

if ($action -eq "selfDeactivate") { 
    $justification = $null; $ticket = $null 
} else {
    $justification = Read-Host "Enter justification (if required)"
    $ticket = Read-Host "Enter ticket info (if required)"
}

#-----------------------------------------------------------
# Build Payload and Set Endpoint Based on Type
#-----------------------------------------------------------
if ($selectedItem.Type -eq "Role") {
    $payload = @{
         "action"           = $action
         "principalId"      = $userId
         "roleDefinitionId" = $selectedItem.RoleDefinitionId
         "directoryScopeId" = "/" 
    }
    if ($action -in @("selfActivate", "extend")) { $payload.scheduleInfo = $scheduleInfo }
    if ($justification) { $payload.justification = $justification }
    if ($ticket) { $payload.ticketInfo = @{ "ticketNumber" = $ticket } }
    $endpoint = "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignmentScheduleRequests"
}
elseif ($selectedItem.Type -eq "Group") {
    $payload = @{
         "action"      = $action
         "principalId" = $userId
         "accessId"    = "member"
         "groupId"     = $selectedItem.GroupId
    }
    if ($action -in @("selfActivate", "extend")) { $payload.scheduleInfo = $scheduleInfo }
    if ($justification) { $payload.justification = $justification }
    if ($ticket) { $payload.ticketInfo = @{ "ticketNumber" = $ticket } }
    $endpoint = "https://graph.microsoft.com/v1.0/identityGovernance/privilegedAccess/group/assignmentScheduleRequests"
}
else { Write-Host "Unknown assignment type." -ForegroundColor Red; exit }

#-----------------------------------------------------------
# For Extensions, perform deactivation then rebuild payload
#-----------------------------------------------------------
if ($action -eq "extend") {
    Write-Host "`nExtending assignment: Sending selfDeactivate request..." -ForegroundColor Cyan
    $deactPayload = $payload.Clone()
    $deactPayload.action = "selfDeactivate"
    $deactPayload.Remove("scheduleInfo") | Out-Null
    $deactPayload.isValidationOnly = $false
    try {
         $jsonDeact = $deactPayload | ConvertTo-Json -Depth 5
         Invoke-MgGraphRequest -Method POST -Uri $endpoint -Body $jsonDeact -ContentType "application/json" | Out-Null
         Write-Host "Deactivation succeeded." -ForegroundColor Green
    }
    catch {
         $errorDetails = Get-ErrorDetails -ErrorRecord $_
         Write-Host "Error during deactivation for extension:" -ForegroundColor Red
         Write-Host $errorDetails.Message -ForegroundColor Red
         Write-Host $errorDetails.Details -ForegroundColor Red
         exit
    }
    if (-not (Wait-ForDeactivation -Type $selectedItem.Type -Id ($selectedItem.Type -eq "Group" ? $selectedItem.GroupId : $selectedItem.RoleDefinitionId))) {
         Write-Host "Timed out waiting for deactivation. Exiting." -ForegroundColor Red
         exit
    }
    
    # Rebuild payload for selfActivate after extension.
    if ($selectedItem.Type -eq "Group") {
         $endpoint = "https://graph.microsoft.com/v1.0/identityGovernance/privilegedAccess/group/assignmentScheduleRequests"
         $payload = @{
              action      = "selfActivate"
              principalId = $userId
              accessId    = "member"
              groupId     = $selectedItem.GroupId
         }
    }
    else {
         $endpoint = "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignmentScheduleRequests"
         $payload = @{
              action           = "selfActivate"
              principalId      = $userId
              roleDefinitionId = $selectedItem.RoleDefinitionId
              directoryScopeId = "/"
         }
    }
    $payload.scheduleInfo = $scheduleInfo
    if ($justification) { $payload.justification = $justification }
    if ($ticket) { $payload.ticketInfo = @{ "ticketNumber" = $ticket } }
    
    Write-Host "`nExtension activation payload:" -ForegroundColor Cyan
    Write-Host ($payload | ConvertTo-Json -Depth 5) -ForegroundColor Cyan
}

#-----------------------------------------------------------
# Validate Payload with Reprompt Logic (for selfActivate and extend)
#-----------------------------------------------------------
if ($action -in @("selfActivate", "extend")) {
    $payload.isValidationOnly = $true
    $payload = Test-Payload -Payload $payload -Endpoint $endpoint
    $payload.isValidationOnly = $false
}

#-----------------------------------------------------------
# Execute API Request
#-----------------------------------------------------------
$jsonPayload = $payload | ConvertTo-Json -Depth 5
Write-Host "`nCalling API at $endpoint with action '$action'..." -ForegroundColor Cyan
try {
     $response = Invoke-MgGraphRequest -Method POST -Uri $endpoint -Body $jsonPayload -ContentType "application/json"
     Write-Host "`nResponse:" -ForegroundColor Green
     $response | Format-Table
}
catch {
     $errorDetails = Get-ErrorDetails -ErrorRecord $_
     Write-Host "`nError during request:" -ForegroundColor Red
     Write-Host $errorDetails.Message -ForegroundColor Red
     Write-Host $errorDetails.Details -ForegroundColor Red
     if ($action -eq "selfDeactivate") { exit }
}

if ($action -eq "selfDeactivate") {
     if (-not (Wait-ForDeactivation -Type $selectedItem.Type -Id ($selectedItem.Type -eq "Group" ? $selectedItem.GroupId : $selectedItem.RoleDefinitionId))) {
          Write-Host "Timed out waiting for deactivation." -ForegroundColor Red
     }
     else {
          Write-Host "Assignment deactivated." -ForegroundColor Green
     }
}