Private/PimRoleChange.ps1

function New-InTUIPimRoleChangeResult {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object]$Role,

        [Parameter(Mandatory)]
        [string]$Status,

        [Parameter()]
        [string]$RequestId,

        [Parameter()]
        [string]$ErrorMessage,

        [Parameter()]
        [object]$RawResponse,

        [Parameter()]
        [switch]$Submitted
    )

    [pscustomobject]@{
        Role        = $Role
        RoleName    = $Role.DisplayName
        Status      = $Status
        RequestId   = $RequestId
        Error       = $ErrorMessage
        RawResponse = $RawResponse
        Submitted   = $Submitted.IsPresent
    }
}

function Get-InTUIPimRoleChangeOperation {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateSet('Activation', 'Deactivation')]
        [string]$Operation
    )

    switch ($Operation) {
        'Activation' {
            $browserSuccessContent = New-InTUIBrowserAuthPageContent `
                -Title 'PIM Activation Successful - InTUI' `
                -Heading 'PIM Activation Successful' `
                -Message 'Your elevated session was refreshed. You can close this window and return to InTUI.'

            return [pscustomobject]@{
                Operation             = 'Activation'
                CompletedStatus       = 'Activated'
                BlockedWarning        = 'Some selected PIM roles are already active and will not be activated again.'
                ResultsTitle          = 'PIM Activation Results'
                SubmitTitle           = '[red]Submitting PIM activation request(s)...[/]'
                RefreshTitle          = '[red]Refreshing activation status...[/]'
                SelectionLabel        = 'Selected roles'
                UsesDuration          = $true
                RequestLogMessage     = 'PIM activation requested'
                FailureLogMessage     = 'PIM activation failed'
                ResponseLogMessage    = 'PIM activation response received'
                BlockLogMessage       = 'PIM activation skipped because role is already active'
                ReauthLoadingTitle    = '[red]Refreshing Graph token after PIM activation...[/]'
                ReauthSuccessMessage  = 'Refreshed Microsoft Graph authentication after successful PIM activation.'
                ReauthFailureMessage  = 'PIM activation succeeded, but Graph token refresh failed. Reconnect manually if Intune access still shows stale authorization.'
                BrowserSuccessContent = $browserSuccessContent
                ValidateRequest       = {
                    param(
                        [Parameter()]
                        [int]$Hours,

                        [Parameter()]
                        [string]$Reason
                    )

                    if ($Hours -lt 1) {
                        throw 'Activation duration is required.'
                    }
                    if ($Hours -gt 24) {
                        throw 'Activation duration cannot exceed 24 hours.'
                    }
                    if (-not (Test-InTUIPimReason -Reason $Reason)) {
                        throw 'Activation reason is required.'
                    }
                }
                GetBlockedMessage     = {
                    param(
                        [Parameter()]
                        [object]$Role,

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

                    if ($ActiveRoleKeys.ContainsKey((Get-InTUIPimRoleKey -Role $Role))) {
                        return 'PIM role is already active. Deactivate it first or wait for it to expire before activating again.'
                    }

                    return $null
                }
                NewRequestBody        = {
                    param(
                        [Parameter()]
                        [object]$Role,

                        [Parameter()]
                        [int]$Hours,

                        [Parameter()]
                        [string]$Reason
                    )

                    New-InTUIPimActivationRequestBody -Role $Role -Hours $Hours -Reason $Reason
                }
                GetLogFields          = {
                    param(
                        [Parameter()]
                        [object]$Role
                    )

                    $null = $Role
                    @{}
                }
            }
        }
        'Deactivation' {
            $browserSuccessContent = New-InTUIBrowserAuthPageContent `
                -Title 'PIM Deactivation Successful - InTUI' `
                -Heading 'PIM Deactivation Successful' `
                -Message 'Your reduced-privilege session was refreshed. You can close this window and return to InTUI.'

            return [pscustomobject]@{
                Operation             = 'Deactivation'
                CompletedStatus       = 'Deactivated'
                BlockedWarning        = 'Some selected PIM roles were activated less than 5 minutes ago and cannot be deactivated yet.'
                ResultsTitle          = 'PIM Deactivation Results'
                SubmitTitle           = '[red]Submitting PIM deactivation request(s)...[/]'
                RefreshTitle          = '[red]Refreshing active role status...[/]'
                SelectionLabel        = 'Selected active roles'
                UsesDuration          = $false
                RequestLogMessage     = 'PIM deactivation requested'
                FailureLogMessage     = 'PIM deactivation failed'
                ResponseLogMessage    = 'PIM deactivation response received'
                BlockLogMessage       = 'PIM deactivation skipped because role was activated too recently'
                ReauthLoadingTitle    = '[red]Refreshing Graph token after PIM deactivation...[/]'
                ReauthSuccessMessage  = 'Refreshed Microsoft Graph authentication after successful PIM deactivation.'
                ReauthFailureMessage  = 'PIM deactivation succeeded, but Graph token refresh failed. Reconnect manually if authorization still shows stale privileges.'
                BrowserSuccessContent = $browserSuccessContent
                ValidateRequest       = {
                    param(
                        [Parameter()]
                        [int]$Hours,

                        [Parameter()]
                        [string]$Reason
                    )

                    $null = $Hours
                    $null = $Reason
                }
                GetBlockedMessage     = {
                    param(
                        [Parameter()]
                        [object]$Role,

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

                    $null = $ActiveRoleKeys
                    Get-InTUIPimDeactivationMinimumAgeMessage -Role $Role
                }
                NewRequestBody        = {
                    param(
                        [Parameter()]
                        [object]$Role,

                        [Parameter()]
                        [int]$Hours,

                        [Parameter()]
                        [string]$Reason
                    )

                    $null = $Hours
                    New-InTUIPimDeactivationRequestBody -Role $Role -Reason $Reason
                }
                GetLogFields          = {
                    param(
                        [Parameter()]
                        [object]$Role
                    )

                    if ($Role.Id) {
                        return @{ TargetScheduleId = $Role.Id }
                    }

                    return @{}
                }
            }
        }
    }
}

function Get-InTUIPimRoleChangeOperationByCompletedStatus {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateSet('Activated', 'Deactivated')]
        [string]$CompletedStatus
    )

    if ($CompletedStatus -eq 'Activated') {
        return (Get-InTUIPimRoleChangeOperation -Operation Activation)
    }

    return (Get-InTUIPimRoleChangeOperation -Operation Deactivation)
}

function New-InTUIPimRoleChangePlan {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateSet('Activation', 'Deactivation')]
        [string]$Operation,

        [Parameter(Mandatory)]
        [object[]]$Roles,

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

    $operationInfo = Get-InTUIPimRoleChangeOperation -Operation $Operation
    $allowedRoles = [System.Collections.Generic.List[object]]::new()
    $blockedResults = [System.Collections.Generic.List[object]]::new()
    $activeRoleKeys = New-InTUIPimRoleKeySet -Roles $ActiveRoles

    foreach ($role in @($Roles)) {
        $blockMessage = & ($operationInfo.GetBlockedMessage) -Role $role -ActiveRoleKeys $activeRoleKeys

        if ($blockMessage) {
            $blockedResults.Add((New-InTUIPimRoleChangeResult -Role $role -Status 'Blocked' -ErrorMessage $blockMessage))
        }
        else {
            $allowedRoles.Add($role)
        }
    }

    [pscustomobject]@{
        Operation      = $operationInfo.Operation
        AllowedRoles    = $allowedRoles.ToArray()
        BlockedResults = $blockedResults.ToArray()
    }
}

function New-InTUIPimActivationPlan {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object[]]$Roles,

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

    New-InTUIPimRoleChangePlan -Operation Activation -Roles $Roles -ActiveRoles $ActiveRoles
}

function New-InTUIPimDeactivationPlan {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object[]]$Roles
    )

    New-InTUIPimRoleChangePlan -Operation Deactivation -Roles $Roles
}

function New-InTUIPimRoleChangeLogContext {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object]$Role,

        [Parameter()]
        [object]$OperationInfo,

        [Parameter()]
        [string]$Reason,

        [Parameter()]
        [int]$Hours,

        [Parameter()]
        [string]$Status,

        [Parameter()]
        [string]$RequestId,

        [Parameter()]
        [string]$ErrorMessage
    )

    $context = @{
        RoleName         = $Role.DisplayName
        RoleDefinitionId = $Role.RoleDefinitionId
        DirectoryScopeId = $Role.DirectoryScopeId
    }

    if ($OperationInfo -and $OperationInfo.GetLogFields) {
        $extraContext = & ($OperationInfo.GetLogFields) -Role $Role
        foreach ($key in @($extraContext.Keys)) {
            $context[$key] = $extraContext[$key]
        }
    }
    if ($Hours -gt 0) { $context['Hours'] = $Hours }
    if ($Reason) { $context['Reason'] = $Reason }
    if ($Status) { $context['Status'] = $Status }
    if ($RequestId) { $context['RequestId'] = $RequestId }
    if ($ErrorMessage) { $context['Error'] = $ErrorMessage }

    return $context
}

function Invoke-InTUIPimRoleChange {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateSet('Activation', 'Deactivation')]
        [string]$Operation,

        [Parameter()]
        [object[]]$Roles,

        [Parameter()]
        [int]$Hours,

        [Parameter()]
        [string]$Reason,

        [Parameter()]
        [object]$Plan
    )

    if ($null -eq $Plan -and $null -eq $Roles) {
        throw "PIM $($Operation.ToLowerInvariant()) requires roles or a role-change plan."
    }

    $operationInfo = Get-InTUIPimRoleChangeOperation -Operation $Operation
    & ($operationInfo.ValidateRequest) -Hours $Hours -Reason $Reason
    $rolePlan = if ($Plan) { $Plan } else { New-InTUIPimRoleChangePlan -Operation $Operation -Roles $Roles }
    if ($rolePlan.Operation -and $rolePlan.Operation -ne $Operation) {
        throw "PIM $($Operation.ToLowerInvariant()) received a $($rolePlan.Operation.ToLowerInvariant()) plan."
    }

    $results = [System.Collections.Generic.List[object]]::new()
    $redactedReason = ConvertTo-InTUIPimRedactedReason -Reason $Reason

    foreach ($blockedResult in @($rolePlan.BlockedResults)) {
        $role = $blockedResult.Role
        Write-InTUILog -Level 'WARN' -Message $operationInfo.BlockLogMessage -Context (New-InTUIPimRoleChangeLogContext -Role $role -OperationInfo $operationInfo -ErrorMessage $blockedResult.Error)
        $results.Add($blockedResult)
    }

    foreach ($role in @($rolePlan.AllowedRoles)) {
        $body = & ($operationInfo.NewRequestBody) -Role $role -Hours $Hours -Reason $Reason

        Write-InTUILog -Message $operationInfo.RequestLogMessage -Context (New-InTUIPimRoleChangeLogContext -Role $role -OperationInfo $operationInfo -Hours $Hours -Reason $redactedReason)

        $response = Invoke-InTUIGraphRequest -Uri '/roleManagement/directory/roleAssignmentScheduleRequests' -Method POST -Body $body -Beta

        if ($null -eq $response) {
            $errorMessage = $script:LastGraphError.Message ?? 'Graph request failed'
            Write-InTUILog -Level 'ERROR' -Message $operationInfo.FailureLogMessage -Context (New-InTUIPimRoleChangeLogContext -Role $role -OperationInfo $operationInfo -Reason $redactedReason -ErrorMessage $errorMessage)
            $results.Add((New-InTUIPimRoleChangeResult -Role $role -Status 'Failed' -ErrorMessage $errorMessage -Submitted))
            continue
        }

        $status = $response.status ?? 'Submitted'
        Write-InTUILog -Message $operationInfo.ResponseLogMessage -Context (New-InTUIPimRoleChangeLogContext -Role $role -OperationInfo $operationInfo -Status $status -RequestId $response.id -Reason $redactedReason)

        $results.Add((New-InTUIPimRoleChangeResult -Role $role -Status $status -RequestId $response.id -RawResponse $response -Submitted))
    }

    return $results.ToArray()
}

function Invoke-InTUIPimRoleActivation {
    [CmdletBinding()]
    param(
        [Parameter()]
        [object[]]$Roles,

        [Parameter()]
        [int]$Hours,

        [Parameter()]
        [string]$Reason,

        [Parameter()]
        [object]$Plan
    )

    Invoke-InTUIPimRoleChange -Operation Activation -Roles $Roles -Plan $Plan -Hours $Hours -Reason $Reason
}

function Invoke-InTUIPimRoleDeactivation {
    [CmdletBinding()]
    param(
        [Parameter()]
        [object[]]$Roles,

        [Parameter()]
        [string]$Reason,

        [Parameter()]
        [object]$Plan
    )

    Invoke-InTUIPimRoleChange -Operation Deactivation -Roles $Roles -Plan $Plan -Reason $Reason
}

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

        [Parameter(Mandatory)]
        [ValidateSet('Activated', 'Deactivated')]
        [string]$CompletedStatus
    )

    $operationInfo = Get-InTUIPimRoleChangeOperationByCompletedStatus -CompletedStatus $CompletedStatus
    $completedResults = @($Results | Where-Object { $_.Status -eq $CompletedStatus })
    if ($completedResults.Count -eq 0) {
        return $false
    }

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

    $reauthScopes = Get-InTUIPimPostRoleChangeRefreshScopes
    $refreshed = Show-InTUILoading -Title $operationInfo.ReauthLoadingTitle -ScriptBlock {
        Reconnect-InTUIGraph `
            -Scopes $reauthScopes `
            -BrowserSuccessContent $operationInfo.BrowserSuccessContent
    }

    if ($refreshed) {
        Show-InTUIInfo $operationInfo.ReauthSuccessMessage
    }
    else {
        Show-InTUIWarning $operationInfo.ReauthFailureMessage
    }

    return $refreshed
}

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

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

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

    $activeKeys = New-InTUIPimRoleKeySet -Roles $ActiveRoles

    foreach ($result in @($Results)) {
        $submittedProperty = $result.PSObject.Properties['Submitted']
        $isSubmitted = ($null -eq $submittedProperty -or $submittedProperty.Value)
        if (-not $isSubmitted -or $result.Status -eq 'Failed' -or $null -eq $result.Role) {
            continue
        }

        $isActive = $activeKeys.ContainsKey((Get-InTUIPimRoleKey -Role $result.Role))
        if (($OperationInfo.CompletedStatus -eq 'Activated' -and $isActive) -or
            ($OperationInfo.CompletedStatus -eq 'Deactivated' -and -not $isActive)) {
            $result.Status = $OperationInfo.CompletedStatus
        }
    }
}

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

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

    $operationInfo = Get-InTUIPimRoleChangeOperation -Operation Activation
    Update-InTUIPimRoleChangeResultsFromActiveRoles -Results $Results -ActiveRoles $ActiveRoles -OperationInfo $operationInfo
}

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

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

    $operationInfo = Get-InTUIPimRoleChangeOperation -Operation Deactivation
    Update-InTUIPimRoleChangeResultsFromActiveRoles -Results $Results -ActiveRoles $ActiveRoles -OperationInfo $operationInfo
}