Private/Authentication/Connect-PIMServices.ps1

function Initialize-WebAssembly {
    <#
    .SYNOPSIS
        Initializes System.Web assembly for URL encoding operations.
     
    .DESCRIPTION
        Loads the System.Web assembly required for HttpUtility.UrlEncode operations
        used in authentication context handling. This is a non-critical operation
        with fallback methods available if loading fails.
     
    .EXAMPLE
        Initialize-WebAssembly
         
        Loads the System.Web assembly for URL encoding functionality.
     
    .NOTES
        This function is called internally during PIM service connections.
        Failure to load this assembly is non-critical as fallback methods exist.
    #>

    [CmdletBinding()]
    param()
    
    try {
        Add-Type -AssemblyName System.Web -ErrorAction Stop
        Write-Verbose "Successfully loaded System.Web assembly"
    }
    catch {
        Write-Verbose "System.Web assembly load failed: $($_.Exception.Message). Using fallback methods."
    }
}

function Connect-PIMServices {
    <#
    .SYNOPSIS
        Establishes authenticated connections to Microsoft Graph and Azure services for PIM operations.
     
    .DESCRIPTION
        Creates authenticated connections to Microsoft services based on the specified role types.
        Handles Microsoft Graph authentication for Entra ID roles and groups, and Azure Resource Manager
        authentication for Azure resource roles. Manages module version conflicts and provides
        comprehensive connection validation.
     
    .PARAMETER IncludeEntraRoles
        Specifies whether to establish connection for Entra ID role management.
        Requires Microsoft Graph connection with appropriate role management scopes.
     
    .PARAMETER IncludeGroups
        Specifies whether to establish connection for privileged group management.
        Requires Microsoft Graph connection with group management scopes.
     
    .PARAMETER IncludeAzureResources
        Specifies whether to establish connection for Azure resource role management.
        Requires both Microsoft Graph and Azure Resource Manager connections.
     
    .PARAMETER ForceNewAccount
        Forces the account picker to appear even if already authenticated.
        Useful for switching between different user accounts or when authentication issues occur.
     
    .OUTPUTS
        PSCustomObject
        Returns a connection result object with the following properties:
        - Success: Boolean indicating overall connection success
        - Error: String containing error message if connection failed
        - GraphContext: Microsoft Graph context object if Graph connection established
        - CurrentUser: Current user object from Microsoft Graph
        - AzureContext: Azure context object if Azure connection established
     
    .EXAMPLE
        Connect-PIMServices -IncludeEntraRoles
         
        Connects to Microsoft Graph for Entra ID role management operations.
     
    .EXAMPLE
        Connect-PIMServices -IncludeEntraRoles -IncludeGroups -IncludeAzureResources
         
        Establishes connections for all PIM operation types: Entra roles, groups, and Azure resources.
     
    .EXAMPLE
        $connection = Connect-PIMServices -IncludeEntraRoles -ForceNewAccount
        if ($connection.Success) {
            Write-Host "Connected as: $($connection.CurrentUser.UserPrincipalName)"
        }
         
        Forces account selection and displays the connected user information.
     
    .NOTES
        This function requires the following PowerShell modules to be installed:
        - Microsoft.Graph.Authentication
        - Microsoft.Graph.Users
        - Microsoft.Graph.Identity.DirectoryManagement
        - Microsoft.Graph.Identity.Governance
        - Az.Accounts (if IncludeAzureResources is specified)
         
        The function automatically handles module version conflicts by removing and reimporting
        the latest available versions.
     
    .LINK
        https://docs.microsoft.com/en-us/powershell/microsoftgraph/
     
    .LINK
        https://docs.microsoft.com/en-us/azure/active-directory/privileged-identity-management/
    #>

    [CmdletBinding()]
    param(
        [Parameter(HelpMessage = "Connect for Entra ID role management")]
        [switch]$IncludeEntraRoles,
        
        [Parameter(HelpMessage = "Connect for privileged group management")]
        [switch]$IncludeGroups,
        
        [Parameter(HelpMessage = "Connect for Azure resource role management")]
        [switch]$IncludeAzureResources,
        
        [Parameter(HelpMessage = "Force account picker to appear")]
        [switch]$ForceNewAccount
    )
    
    # Initialize connection result object
    $result = [PSCustomObject]@{
        Success = $false
        Error = $null
        GraphContext = $null
        CurrentUser = $null
        AzureContext = $null
    }
    
    try {
        # Establish Microsoft Graph connection if required
        if ($IncludeEntraRoles -or $IncludeGroups) {
            Write-Verbose "Initializing Microsoft Graph connection..."
            
            # Define required Graph modules
            $requiredModules = @(
                'Microsoft.Graph.Authentication',
                'Microsoft.Graph.Users',
                'Microsoft.Graph.Identity.DirectoryManagement',
                'Microsoft.Graph.Identity.Governance',
                'MSAL.PS'
            )
            
            # Resolve module version conflicts and ensure latest versions are loaded
            foreach ($moduleName in $requiredModules) {
                $loadedModules = @(Get-Module -Name $moduleName -ErrorAction SilentlyContinue)
                
                if ($loadedModules.Count -gt 1) {
                    Write-Verbose "Resolving version conflict for $moduleName..."
                    $loadedModules | Remove-Module -Force -ErrorAction SilentlyContinue
                    
                    $latestModule = Get-Module -ListAvailable -Name $moduleName | 
                                   Sort-Object Version -Descending | 
                                   Select-Object -First 1
                    
                    if ($latestModule) {
                        Import-Module -Name $moduleName -RequiredVersion $latestModule.Version -Force -ErrorAction Stop
                        Write-Verbose "Loaded $moduleName v$($latestModule.Version)"
                    }
                }
                elseif ($loadedModules.Count -eq 0) {
                    $availableModule = Get-Module -ListAvailable -Name $moduleName | 
                                      Sort-Object Version -Descending | 
                                      Select-Object -First 1
                    
                    if ($availableModule) {
                        Import-Module -Name $moduleName -RequiredVersion $availableModule.Version -Force -ErrorAction Stop
                        Write-Verbose "Loaded $moduleName v$($availableModule.Version)"
                    }
                    else {
                        $result.Error = "Required module '$moduleName' not found. Install with: Install-Module -Name $moduleName"
                        return $result
                    }
                }
            }
            
            # Initialize System.Web assembly for URL encoding
            Initialize-WebAssembly
            
            # Clear existing Graph context if forced account selection requested
            if ($ForceNewAccount) {
                Write-Verbose "Clearing existing authentication context..."
                
                # Preserve authentication context token if it exists and is recent
                $preserveAuthToken = $false
                if ($script:CurrentAuthContextToken -and $script:AuthContextCompletionTime) {
                    $timeSinceAuth = (Get-Date) - $script:AuthContextCompletionTime
                    if ($timeSinceAuth.TotalMinutes -lt 30) {  # Preserve token if less than 30 minutes old
                        $preserveAuthToken = $true
                        Write-Verbose "Preserving recent authentication context token (age: $([math]::Round($timeSinceAuth.TotalMinutes, 2)) minutes)"
                    }
                }
                
                for ($i = 0; $i -lt 3; $i++) {
                    Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
                    Start-Sleep -Milliseconds 200
                }
                
                # Clear token cache but preserve auth context token if recent
                if (-not $preserveAuthToken) {
                    Write-Verbose "Clearing authentication context tokens (too old or doesn't exist)"
                    $script:CurrentAuthContextToken = $null
                    $script:CurrentAuthContextRefreshToken = $null
                    $script:AuthContextTokens = @{}
                    $script:JustCompletedAuthContext = $false
                    $script:AuthContextCompletionTime = $null
                }
            }
            
            # Define Microsoft Graph permission scopes
            $graphScopes = @(
                'User.Read'
                'Directory.Read.All'
                'RoleEligibilitySchedule.ReadWrite.Directory'
                'RoleAssignmentSchedule.ReadWrite.Directory'
                'PrivilegedAccess.ReadWrite.AzureADGroup'
                'RoleManagementPolicy.Read.Directory'
                'RoleManagementPolicy.Read.AzureADGroup'
                'Policy.Read.ConditionalAccess'
            )
                        
            try {
                # Establish Graph connection
                Write-Verbose "Authenticating to Microsoft Graph..."
                Connect-MgGraph -Scopes $graphScopes -NoWelcome -ErrorAction Stop
                
                # Validate connection establishment
                $context = Get-MgContext
                if (-not $context) {
                    $result.Error = "Microsoft Graph connection failed - no authentication context available"
                    return $result
                }
                
                $result.GraphContext = $context
                Write-Verbose "Microsoft Graph connection established successfully"
                
                # Retrieve current user information
                if ($context.Account) {
                    Write-Verbose "Retrieving current user profile..."
                    $currentUser = Get-MgUser -UserId $context.Account -ErrorAction Stop
                    $result.CurrentUser = $currentUser
                    Write-Verbose "Authenticated as: $($currentUser.UserPrincipalName)"
                    
                    # Persist last used account for future sessions
                    try {
                        Save-LastUsedAccount -UserPrincipalName $currentUser.UserPrincipalName
                    }
                    catch {
                        Write-Verbose "Unable to save account preference: $($_.Exception.Message)"
                    }
                }
            }
            catch {
                $result.Error = "Microsoft Graph authentication failed: $($_.Exception.Message)"
                Write-Verbose "Graph connection error: $($_.Exception.GetType().Name) - $($_.Exception.Message)"
                return $result
            }
        }
        
        # Establish Azure Resource Manager connection if required
        if ($IncludeAzureResources) {
            Write-Warning "Azure Resource role management is not yet implemented. Skipping Azure connection for version 2.0.0."
            Write-Verbose "Azure resource support placeholder - connection skipped"
            
            # Set result properties to indicate feature not available
            $result.AzureContext = [PSCustomObject]@{
                Status = "Not Implemented"
                Message = "Azure Resource support planned for version 2.0.0"
            }
        }
        
        $result.Success = $true
        Write-Verbose "All requested service connections established successfully"
    }
    catch {
        $result.Error = "Service connection failed: $($_.Exception.Message)"
        Write-Verbose "Unexpected error: $($_.Exception.GetType().Name) - $($_.Exception.Message)"
    }
    
    return $result
}