Views/PimRoleActivation.ps1

function Show-InTUIPimScreen {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string[]]$BreadcrumbPath
    )

    Clear-Host
    Show-InTUIHeader
    Show-InTUIBreadcrumb -Path $BreadcrumbPath
}

function Test-InTUIPimInteractiveConnection {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$ActionVerb,

        [Parameter(Mandatory)]
        [string]$ActionNoun
    )

    if (-not $script:Connected) {
        Show-InTUIWarning "Connect to Microsoft Graph before $ActionVerb PIM roles."
        Read-InTUIKey
        return $false
    }

    if (-not (Test-InTUIPimDelegatedContext)) {
        Show-InTUIError "PIM role $ActionNoun requires an interactive delegated user connection. Service principal connections are not supported."
        Read-InTUIKey
        return $false
    }

    return $true
}

function Invoke-InTUIPimDataLoadWithReconnect {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [scriptblock]$LoadData,

        [Parameter(Mandatory)]
        [string[]]$BreadcrumbPath
    )

    for ($attempt = 0; $attempt -lt 2; $attempt++) {
        $data = & $LoadData
        if (-not $data.PermissionError) {
            return $data
        }

        Show-InTUIPimPermissionWarning
        if (-not (Show-InTUIConfirm -Message "[yellow]Reconnect with PIM permissions now?[/]")) {
            Read-InTUIKey
            return $null
        }

        if (-not (Connect-InTUIPimPermissions)) {
            Show-InTUIError 'Reconnect with PIM permissions failed.'
            Read-InTUIKey
            return $null
        }

        Show-InTUIPimScreen -BreadcrumbPath $BreadcrumbPath
    }

    return $data
}

function Show-InTUIPimRoleActivation {
    [CmdletBinding()]
    param()

    $breadcrumbPath = @('Home', 'Security', 'Entra ID PIM Role Activation')

    Show-InTUIPimScreen -BreadcrumbPath $breadcrumbPath

    if (-not (Test-InTUIPimInteractiveConnection -ActionVerb 'activating' -ActionNoun 'activation')) {
        return
    }

    $data = Invoke-InTUIPimDataLoadWithReconnect -LoadData { Get-InTUIPimRoleActivationData } -BreadcrumbPath $breadcrumbPath

    if ($null -eq $data -or $data.PermissionError) {
        Show-InTUIPimPermissionWarning
        Read-InTUIKey
        return
    }

    $eligibleRoles = @($data.Eligible)
    $activeRoles = @($data.Active)
    $availableRoles = @($eligibleRoles)

    if ($activeRoles.Count -gt 0) {
        Show-InTUIInfo "$($activeRoles.Count) active role assignment(s) found. Eligible roles are still shown for activation and labeled Active."
        Write-InTUIText (Get-InTUIPimActiveRoleSummary -Roles $activeRoles)
    }

    if ($availableRoles.Count -eq 0) {
        Show-InTUIWarning "No eligible Entra ID directory roles found for this account."
        Write-InTUIText "[grey]- You may not have direct eligible PIM assignments.[/]"
        Write-InTUIText "[grey]- Group-based PIM eligibility is not included in this view.[/]"
        Write-InTUIText "[grey]- The current connection may lack PIM permissions.[/]"
        Read-InTUIKey
        return
    }

    $roleChoices = @(Get-InTUIPimActivationRoleChoices -EligibleRoles $availableRoles -ActiveRoles $activeRoles)

    $choiceMap = Get-InTUIChoiceMap -Choices $roleChoices
    $selectedChoices = @(Show-InTUIMultiSelect -Title "[red]Select PIM roles to activate[/]" -Choices $choiceMap.Choices -PageSize 15)
    if ($selectedChoices.Count -eq 0) {
        return
    }

    $selectedRoles = @(Resolve-InTUIPimSelectedRole -SelectedChoices $selectedChoices -ChoiceMap $choiceMap -AvailableRoles $availableRoles)
    if ($selectedRoles.Count -eq 0) {
        return
    }

    $hours = Read-InTUIPimDurationInput -MaximumHours 8
    if ($null -eq $hours) {
        return
    }

    $reason = Read-InTUIPimReasonInput
    if (-not (Test-InTUIPimReason -Reason $reason)) {
        return
    }

    if (-not (Confirm-InTUIPimActivation -Roles $selectedRoles -Hours $hours -Reason $reason)) {
        return
    }

    $results = Show-InTUILoading -Title "[red]Submitting PIM activation request(s)...[/]" -ScriptBlock {
        Invoke-InTUIPimRoleActivation -Roles $selectedRoles -Hours $hours -Reason $reason
    }

    Start-Sleep -Seconds 2
    $refreshedActive = Show-InTUILoading -Title "[red]Refreshing activation status...[/]" -ScriptBlock {
        @(Get-InTUIPimActiveDirectoryRole)
    }
    Update-InTUIPimActivationResultsFromActiveRoles -Results $results -ActiveRoles $refreshedActive

    $hasActivatedResults = @($results | Where-Object { $_.Status -eq 'Activated' }).Count -gt 0
    if ($hasActivatedResults) {
        if (Complete-InTUIPimActivationReauth -Results $results) {
            Show-InTUIInfo 'Refreshed Microsoft Graph authentication after successful PIM activation.'
        }
        else {
            Show-InTUIWarning 'PIM activation succeeded, but Graph token refresh failed. Reconnect manually if Intune access still shows stale authorization.'
        }
    }

    Show-InTUIPimActivationResults -Results $results
    Read-InTUIKey
}

function Show-InTUIPimRoleDeactivation {
    [CmdletBinding()]
    param()

    $breadcrumbPath = @('Home', 'Security', 'Entra ID PIM Role Deactivation')

    Show-InTUIPimScreen -BreadcrumbPath $breadcrumbPath

    if (-not (Test-InTUIPimInteractiveConnection -ActionVerb 'deactivating' -ActionNoun 'deactivation')) {
        return
    }

    $data = Invoke-InTUIPimDataLoadWithReconnect -LoadData { Get-InTUIPimActiveRoleData } -BreadcrumbPath $breadcrumbPath

    if ($null -eq $data -or $data.PermissionError) {
        Show-InTUIPimPermissionWarning
        Read-InTUIKey
        return
    }

    $activeRoles = @($data.Active)
    if ($activeRoles.Count -eq 0) {
        Show-InTUIWarning "No active Entra ID PIM directory roles found for this account."
        Write-InTUIText "[grey]- Activate a role first, or wait for Graph to reflect the active assignment.[/]"
        Write-InTUIText "[grey]- Group-based PIM activation is not included in this view.[/]"
        Read-InTUIKey
        return
    }

    $roleChoices = @()
    foreach ($role in $activeRoles) {
        $scope = Get-InTUIPimScopeLabel -DirectoryScopeId $role.DirectoryScopeId
        $roleChoices += "[white]$(ConvertTo-InTUISafeMarkup -Text $role.DisplayName)[/] [grey]| Scope: $scope[/]"
    }

    $choiceMap = Get-InTUIChoiceMap -Choices $roleChoices
    $selectedChoices = @(Show-InTUIMultiSelect -Title "[red]Select active PIM roles to deactivate[/]" -Choices $choiceMap.Choices -PageSize 15)
    if ($selectedChoices.Count -eq 0) {
        return
    }

    $selectedRoles = @(Resolve-InTUIPimSelectedRole -SelectedChoices $selectedChoices -ChoiceMap $choiceMap -AvailableRoles $activeRoles)
    if ($selectedRoles.Count -eq 0) {
        return
    }

    $reason = Read-InTUIPimOptionalReasonInput
    if (-not (Confirm-InTUIPimDeactivation -Roles $selectedRoles -Reason $reason)) {
        return
    }

    $results = Show-InTUILoading -Title "[red]Submitting PIM deactivation request(s)...[/]" -ScriptBlock {
        Invoke-InTUIPimRoleDeactivation -Roles $selectedRoles -Reason $reason
    }

    Start-Sleep -Seconds 2
    $refreshedActive = Show-InTUILoading -Title "[red]Refreshing active role status...[/]" -ScriptBlock {
        @(Get-InTUIPimActiveDirectoryRole)
    }
    Update-InTUIPimDeactivationResultsFromActiveRoles -Results $results -ActiveRoles $refreshedActive

    Show-InTUIPimActivationResults -Title 'PIM Deactivation Results' -Results $results
    Read-InTUIKey
}

function Get-InTUIPimRoleActivationData {
    [CmdletBinding()]
    param()

    Show-InTUILoading -Title "[red]Loading eligible PIM roles...[/]" -ScriptBlock {
        $script:LastGraphError = $null
        $eligible = @(Get-InTUIPimEligibleDirectoryRole)
        if (Test-InTUIPimPermissionError -ErrorInfo $script:LastGraphError) {
            return @{ PermissionError = $true; Eligible = @(); Active = @() }
        }

        $active = @(Get-InTUIPimActiveDirectoryRole)
        if (Test-InTUIPimPermissionError -ErrorInfo $script:LastGraphError) {
            return @{ PermissionError = $true; Eligible = @(); Active = @() }
        }

        @{
            PermissionError = $false
            Eligible        = $eligible
            Active          = $active
        }
    }
}

function Get-InTUIPimActiveRoleData {
    [CmdletBinding()]
    param()

    Show-InTUILoading -Title "[red]Loading active PIM roles...[/]" -ScriptBlock {
        $script:LastGraphError = $null
        $active = @(Get-InTUIPimActiveDirectoryRole)
        if (Test-InTUIPimPermissionError -ErrorInfo $script:LastGraphError) {
            return @{ PermissionError = $true; Active = @() }
        }

        @{
            PermissionError = $false
            Active          = $active
        }
    }
}

function Connect-InTUIPimPermissions {
    [CmdletBinding()]
    param()

    $tenantId = $script:TenantId
    $environment = if ($script:CloudEnvironment) { $script:CloudEnvironment } else { 'Global' }

    Reconnect-InTUIGraph -TenantId $tenantId -Environment $environment -Scopes (Get-InTUIPimConnectionScopes) -UseDeviceCode:$script:UseDeviceCode
}

function Get-InTUIPimRoleChoiceLabel {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object]$Role,

        [Parameter()]
        [hashtable]$ActiveRoleKeys = @{}
    )

    $scope = Get-InTUIPimScopeLabel -DirectoryScopeId $Role.DirectoryScopeId
    $displayName = ConvertTo-InTUISafeMarkup -Text $Role.DisplayName
    $activeLabel = if ($ActiveRoleKeys.ContainsKey((Get-InTUIPimRoleKey -Role $Role))) {
        ' [green]| Active[/]'
    }
    else {
        ''
    }

    return "[white]$displayName[/]$activeLabel [grey]| Scope: $scope[/]"
}

function Get-InTUIPimActivationRoleChoices {
    [CmdletBinding()]
    param(
        [Parameter()]
        [object[]]$EligibleRoles = @(),

        [Parameter()]
        [object[]]$ActiveRoles = @()
    )

    $activeRoleKeys = @{}
    foreach ($role in @($ActiveRoles)) {
        $activeRoleKeys[(Get-InTUIPimRoleKey -Role $role)] = $true
    }

    $roleChoices = @()
    foreach ($role in @($EligibleRoles)) {
        $roleChoices += Get-InTUIPimRoleChoiceLabel -Role $role -ActiveRoleKeys $activeRoleKeys
    }

    return $roleChoices
}

function Format-InTUIPimExpirationDisplay {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$EndDateTime
    )

    if ([string]::IsNullOrWhiteSpace($EndDateTime)) {
        return $null
    }

    try {
        $expiry = [System.DateTimeOffset]::Parse($EndDateTime)
        $localExpiry = $expiry.ToLocalTime().ToString('yyyy-MM-dd HH:mm')
        $remaining = $expiry - [System.DateTimeOffset]::UtcNow

        if ($remaining.TotalMinutes -le 0) {
            return "$localExpiry (expired)"
        }

        if ($remaining.TotalHours -lt 1) {
            return "$localExpiry ($([math]::Ceiling($remaining.TotalMinutes))m left)"
        }

        if ($remaining.TotalDays -lt 1) {
            return "$localExpiry ($([math]::Floor($remaining.TotalHours))h left)"
        }

        return "$localExpiry ($([math]::Floor($remaining.TotalDays))d left)"
    }
    catch {
        return $EndDateTime
    }
}

function Get-InTUIPimActiveRoleSummary {
    [CmdletBinding()]
    param(
        [Parameter()]
        [object[]]$Roles = @()
    )

    if ($Roles.Count -eq 0) {
        return '[grey]Active now:[/] [grey]None[/]'
    }

    $roleLabels = @($Roles | Sort-Object DisplayName, DirectoryScopeId | ForEach-Object {
        $scope = Get-InTUIPimScopeLabel -DirectoryScopeId $_.DirectoryScopeId
        $expiration = Format-InTUIPimExpirationDisplay -EndDateTime $_.EndDateTime
        $details = if ([string]::IsNullOrWhiteSpace($expiration)) {
            $scope
        }
        else {
            "$scope | Expires: $expiration"
        }

        " [green]$(ConvertTo-InTUISafeMarkup -Text $_.DisplayName)[/] [grey]($details)[/]"
    })

    return "[grey]Active now:[/]`n$($roleLabels -join "`n")"
}

function Complete-InTUIPimActivationReauth {
    [CmdletBinding()]
    param(
        [Parameter()]
        [object[]]$Results = @()
    )

    $activatedResults = @($Results | Where-Object { $_.Status -eq 'Activated' })
    if ($activatedResults.Count -eq 0) {
        return $false
    }

    $roleNames = @($activatedResults | ForEach-Object { $_.RoleName } | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })
    Write-InTUILog -Message 'Refreshing Graph token after PIM activation' -Context @{
        ActivatedCount = $activatedResults.Count
        Roles          = ($roleNames -join ',')
    }

    $context = Get-MgContext -ErrorAction SilentlyContinue
    $reauthScopes = Get-InTUIPimReauthScopes -Scopes $context.Scopes

    return (Show-InTUILoading -Title "[red]Refreshing Graph token after PIM activation...[/]" -ScriptBlock {
        Reconnect-InTUIGraph -Scopes $reauthScopes -UseDeviceCode:$script:UseDeviceCode
    })
}

function Resolve-InTUIPimSelectedRole {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string[]]$SelectedChoices,

        [Parameter(Mandatory)]
        [hashtable]$ChoiceMap,

        [Parameter(Mandatory)]
        [object[]]$AvailableRoles
    )

    $selectedRoles = @()
    foreach ($choice in $SelectedChoices) {
        $idx = $ChoiceMap.IndexMap[$choice]
        if ($null -ne $idx -and $idx -lt $AvailableRoles.Count) {
            $selectedRoles += $AvailableRoles[$idx]
        }
    }

    return $selectedRoles
}

function Read-InTUIPimDurationInput {
    [CmdletBinding()]
    param(
        [Parameter()]
        [int]$MaximumHours = 8
    )

    while ($true) {
        $value = Read-InTUITextInput -Message "[red]Duration in hours[/]" -DefaultAnswer '1'
        if ([string]::IsNullOrWhiteSpace($value)) {
            return $null
        }

        $hours = 0
        if ([int]::TryParse($value, [ref]$hours) -and $hours -ge 1 -and $hours -le $MaximumHours) {
            return $hours
        }

        Show-InTUIWarning "Enter a whole number from 1 to $MaximumHours."
    }
}

function Read-InTUIPimReasonInput {
    [CmdletBinding()]
    param()

    while ($true) {
        $reason = Read-InTUITextInput -Message "[red]Reason for activation[/]"
        if (Test-InTUIPimReason -Reason $reason) {
            return $reason.Trim()
        }

        Show-InTUIWarning 'Activation reason is required.'
    }
}

function Read-InTUIPimOptionalReasonInput {
    [CmdletBinding()]
    param()

    $reason = Read-InTUITextInput -Message "[red]Reason for deactivation[/] [grey](optional, press Enter to skip)[/]"
    if ([string]::IsNullOrWhiteSpace($reason)) {
        return ''
    }

    return $reason.Trim()
}

function Confirm-InTUIPimActivation {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object[]]$Roles,

        [Parameter(Mandatory)]
        [int]$Hours,

        [Parameter(Mandatory)]
        [string]$Reason
    )

    $roleLines = @($Roles | ForEach-Object {
        $scope = Get-InTUIPimScopeLabel -DirectoryScopeId $_.DirectoryScopeId
        "- $($_.DisplayName) ($scope)"
    })
    $content = @"
[bold white]Selected roles:[/]
$($roleLines -join "`n")

[grey]Duration:[/] $Hours hour(s)
[grey]Reason:[/] $Reason
"@


    Show-InTUIPanel -Title "[red]Review PIM Activation[/]" -Content $content -BorderColor Red
    return (Show-InTUIConfirm -Message "[yellow]Submit activation request(s)?[/]")
}

function Confirm-InTUIPimDeactivation {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object[]]$Roles,

        [Parameter()]
        [string]$Reason
    )

    $roleLines = @($Roles | ForEach-Object {
        $scope = Get-InTUIPimScopeLabel -DirectoryScopeId $_.DirectoryScopeId
        "- $($_.DisplayName) ($scope)"
    })
    $reasonLine = if ([string]::IsNullOrWhiteSpace($Reason)) { 'N/A' } else { $Reason }
    $content = @"
[bold white]Selected active roles:[/]
$($roleLines -join "`n")

[grey]Reason:[/] $reasonLine
"@


    Show-InTUIPanel -Title "[red]Review PIM Deactivation[/]" -Content $content -BorderColor Red
    return (Show-InTUIConfirm -Message "[yellow]Submit deactivation request(s)?[/]")
}

function Update-InTUIPimActivationResultsFromActiveRoles {
    [CmdletBinding()]
    param(
        [Parameter()]
        [object[]]$Results = @(),

        [Parameter()]
        [object[]]$ActiveRoles = @()
    )

    $activeKeys = @{}
    foreach ($role in @($ActiveRoles)) {
        $activeKeys[(Get-InTUIPimRoleKey -Role $role)] = $true
    }

    foreach ($result in @($Results)) {
        if ($result.Status -eq 'Failed' -or $null -eq $result.Role) {
            continue
        }

        if ($activeKeys.ContainsKey((Get-InTUIPimRoleKey -Role $result.Role))) {
            $result.Status = 'Activated'
        }
    }
}

function Update-InTUIPimDeactivationResultsFromActiveRoles {
    [CmdletBinding()]
    param(
        [Parameter()]
        [object[]]$Results = @(),

        [Parameter()]
        [object[]]$ActiveRoles = @()
    )

    $activeKeys = @{}
    foreach ($role in @($ActiveRoles)) {
        $activeKeys[(Get-InTUIPimRoleKey -Role $role)] = $true
    }

    foreach ($result in @($Results)) {
        if ($result.Status -eq 'Failed' -or $null -eq $result.Role) {
            continue
        }

        if (-not $activeKeys.ContainsKey((Get-InTUIPimRoleKey -Role $result.Role))) {
            $result.Status = 'Deactivated'
        }
    }
}

function Show-InTUIPimActivationResults {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$Title = 'PIM Activation Results',

        [Parameter()]
        [object[]]$Results = @()
    )

    $rows = @()
    foreach ($result in @($Results)) {
        $statusColor = switch -Regex ($result.Status) {
            'Activated|Granted|Provisioned' { 'green' }
            'Pending|Approval'             { 'yellow' }
            'Failed|Denied'                { 'red' }
            default                        { 'blue' }
        }
        $detail = if ($result.Error) { $result.Error } elseif ($result.RequestId) { $result.RequestId } else { 'N/A' }
        $rows += , @(
            ($result.RoleName ?? 'Unknown role'),
            "[$statusColor]$($result.Status)[/]",
            $detail
        )
    }

    Show-InTUITable -Title $Title -Columns @('Role', 'Status', 'Request/Error') -Rows $rows -BorderColor Red
}

function Show-InTUIPimPermissionWarning {
    [CmdletBinding()]
    param()

    $scopes = (Get-InTUIPimRequiredScopes) -join ', '
    Show-InTUIWarning "PIM role activation requires delegated Graph permissions: $scopes."
}