Private/RoleManagement/Get-RoleActivationParameters.ps1

function Get-RoleActivationParameters {
    <#
    .SYNOPSIS
        Builds role activation request parameters for Microsoft Graph and Azure Resource PIM.
 
    .DESCRIPTION
        Creates the scheduleInfo and role-specific payload fields needed to submit PIM activation
        requests. The function supports immediate and scheduled activations by accepting an optional
        local start time and converting it to the UTC timestamp format expected by Graph and ARM.
 
    .PARAMETER RoleData
        Role metadata for the selected Entra, Group, or Azure Resource role.
 
    .PARAMETER Justification
        Justification text to include with the activation request.
 
    .PARAMETER EffectiveDuration
        Hashtable containing the duration after policy maximums have been applied.
 
    .PARAMETER TicketInfo
        Optional ticket metadata when the role policy requires ticketing.
 
    .PARAMETER ScheduleStartTime
        Optional local date and time when the activation should start. When omitted, the request
        starts immediately.
 
    .PARAMETER AzureTargetScope
        Optional reduced Azure Resource scope to use for Azure role activation.
 
    .PARAMETER LinkedRoleEligibilityScheduleId
        Optional Azure eligibility schedule ID required when activating at a reduced scope.
 
    .OUTPUTS
        Hashtable
        Returns a Graph-compatible activation hashtable, or an Azure Resource activation hashtable
        for Azure Resource roles.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object]$RoleData,
        
        [Parameter(Mandatory)]
        [string]$Justification,
        
        [Parameter(Mandatory)]
        [hashtable]$EffectiveDuration,
        
        [hashtable]$TicketInfo,

        [datetime]$ScheduleStartTime,

        [string]$AzureTargetScope,

        [string]$LinkedRoleEligibilityScheduleId
    )

    $activationStartDateTime = if ($PSBoundParameters.ContainsKey('ScheduleStartTime')) { [datetime]$ScheduleStartTime } else { Get-Date }
    if ($activationStartDateTime.Kind -eq [System.DateTimeKind]::Unspecified) {
        $activationStartDateTime = [datetime]::SpecifyKind($activationStartDateTime, [System.DateTimeKind]::Local)
    }
    else {
        $activationStartDateTime = $activationStartDateTime.ToLocalTime()
    }
    $activationStartDateTimeUtc = $activationStartDateTime.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss.fff'Z'")
    
    $activationParams = @{
        action        = "selfActivate"
        justification = $Justification
        principalId   = $script:CurrentUser.Id
        scheduleInfo  = @{
            startDateTime = $activationStartDateTimeUtc
            expiration    = @{
                duration = "PT$($EffectiveDuration.Hours)H$($EffectiveDuration.Minutes)M"
                type     = "afterDuration"
            }
        }
    }
    
    # Add role-specific parameters
    switch ($RoleData.Type) {
        'Entra' {
            $activationParams.roleDefinitionId = $RoleData.RoleDefinitionId
            $activationParams.directoryScopeId = if ($RoleData.DirectoryScopeId) { $RoleData.DirectoryScopeId } else { "/" }
        }
        
        'Group' {
            $groupAccessCandidates = @()
            if ($RoleData.PSObject.Properties['AccessId'] -and $RoleData.AccessId) {
                $groupAccessCandidates += $RoleData.AccessId
            }
            if ($RoleData.PSObject.Properties['Assignment'] -and $RoleData.Assignment -and $RoleData.Assignment.PSObject.Properties['AccessId'] -and $RoleData.Assignment.AccessId) {
                $groupAccessCandidates += $RoleData.Assignment.AccessId
            }
            if ($RoleData.PSObject.Properties['MemberType'] -and $RoleData.MemberType) {
                $groupAccessCandidates += $RoleData.MemberType
            }
            if ($RoleData.PSObject.Properties['MembershipType'] -and $RoleData.MembershipType) {
                $groupAccessCandidates += $RoleData.MembershipType
            }

            $groupAccessId = 'member'
            foreach ($candidate in $groupAccessCandidates) {
                $normalizedAccessId = ([string]$candidate).Trim().ToLowerInvariant()
                if ($normalizedAccessId -in @('member', 'owner')) {
                    $groupAccessId = $normalizedAccessId
                    break
                }
            }

            $activationParams.groupId = $RoleData.GroupId
            $activationParams.accessId = $groupAccessId
        }
        
        'AzureResource' {
            $originalScope = if ($RoleData.PSObject.Properties['FullScope'] -and $RoleData.FullScope) {
                $RoleData.FullScope
            }
            elseif ($RoleData.PSObject.Properties['Scope'] -and $RoleData.Scope) {
                $RoleData.Scope
            }
            elseif ($RoleData.PSObject.Properties['DirectoryScopeId'] -and $RoleData.DirectoryScopeId) {
                $RoleData.DirectoryScopeId
            }
            else {
                throw "Azure Resource role '$($RoleData.DisplayName)' is missing an activation scope."
            }
            $targetScope = if (-not [string]::IsNullOrWhiteSpace($AzureTargetScope)) { $AzureTargetScope } else { $originalScope }
            $scopeValidation = Test-AzureReducedScope -OriginalScope $originalScope -TargetScope $targetScope

            if (-not $scopeValidation.IsValid) {
                throw $scopeValidation.ErrorMessage
            }

            $effectiveScope = $scopeValidation.TargetScope
            $originalScope = $scopeValidation.OriginalScope
            $isReducedScope = [bool]$scopeValidation.IsReducedScope
            $linkedEligibilityId = if ($LinkedRoleEligibilityScheduleId) {
                $LinkedRoleEligibilityScheduleId
            }
            elseif ($isReducedScope -and $RoleData.PSObject.Properties['EligibilityScheduleName'] -and $RoleData.EligibilityScheduleName) {
                $RoleData.EligibilityScheduleName
            }
            elseif ($isReducedScope -and $RoleData.PSObject.Properties['EligibilityScheduleId'] -and $RoleData.EligibilityScheduleId) {
                $RoleData.EligibilityScheduleId
            }
            else {
                $null
            }

            $roleDefinitionId = $RoleData.RoleDefinitionId
            if ($roleDefinitionId -and -not $roleDefinitionId.StartsWith('/')) {
                if ($originalScope -match '^/subscriptions/([a-fA-F0-9\-]{36})') {
                    $roleDefinitionId = "/subscriptions/$($matches[1])/providers/Microsoft.Authorization/roleDefinitions/$roleDefinitionId"
                }
                elseif ($effectiveScope -match '^/subscriptions/([a-fA-F0-9\-]{36})') {
                    $roleDefinitionId = "/subscriptions/$($matches[1])/providers/Microsoft.Authorization/roleDefinitions/$roleDefinitionId"
                }
                else {
                    $roleDefinitionId = "/providers/Microsoft.Authorization/roleDefinitions/$roleDefinitionId"
                }
            }

            # Azure Resource roles use different parameter structure
            return @{
                Scope                           = $effectiveScope
                OriginalScope                   = $originalScope
                IsReducedScope                  = $isReducedScope
                LinkedRoleEligibilityScheduleId = if ($isReducedScope) { $linkedEligibilityId } else { $null }
                RoleDefinitionId                = $roleDefinitionId
                PrincipalId                     = $script:CurrentUser.Id
                RequestType                     = 'SelfActivate'
                Justification                   = $Justification
                ScheduleInfo                    = @{
                    StartDateTime = $activationStartDateTimeUtc
                    Expiration    = @{
                        Type     = 'AfterDuration'
                        Duration = "PT$($EffectiveDuration.Hours)H$($EffectiveDuration.Minutes)M"
                    }
                }
                TicketInfo                      = if ($TicketInfo -and $TicketInfo.ticketNumber) { $TicketInfo } else { $null }
            }
        }
    }
    
    # Add ticket info for Entra/Group roles if present
    if ($TicketInfo -and $TicketInfo.ticketNumber) {
        $activationParams.ticketInfo = $TicketInfo
    }
    
    return $activationParams
}