Private/NC-Hlp.Connections.ps1

#Requires -Version 5.0
using namespace System.Management.Automation

# Nebula.Core: (Private) Connections helpers ========================================================================================================

function Test-EOLConnection {
    <#
    .SYNOPSIS
        Makes sure the Exchange Online session is ready to use.
    .DESCRIPTION
        Verifies that the ExchangeOnlineManagement module is present (installing it if asked),
        checks the current session and, if necessary, reconnects with the detected user. The
        combined Connect-Nebula flow can disable WAM after Graph initializes its dependencies.
    .PARAMETER UserPrincipalName
        Optional explicit UPN to use when connecting. Defaults to Find-UserConnected.
    .PARAMETER AutoInstall
        Install the module automatically instead of prompting.
    .PARAMETER ForceReconnect
        Skip the quick connectivity probe and always reconnect.
    .PARAMETER DisableWAM
        Disable Web Account Manager for the EXO sign-in flow.
    #>

    [CmdletBinding()]
    param(
        [string]$UserPrincipalName,
        [switch]$AutoInstall,
        [switch]$ForceReconnect,
        [switch]$DisableWAM
    )

    $moduleAvailable = @(Get-Module -Name ExchangeOnlineManagement -ListAvailable).Count -gt 0
    if (-not $moduleAvailable) {
        Write-NCMessage "Microsoft Exchange Online Management module is not available." -Level WARNING

        $installModule = $AutoInstall.IsPresent
        if (-not $installModule) {
            $confirmation = Read-Host "Install Microsoft Exchange Online Management module now? [Y] Yes [N] No"
            $installModule = $confirmation -match '^[yY]'
        }

        if (-not $installModule) {
            Add-EmptyLine
            Write-NCMessage "Microsoft Exchange Online Management module is required. Install it with Install-Module ExchangeOnlineManagement." -Level ERROR
            return $false
        }

        try {
            Write-Verbose "Installing Microsoft Exchange Online Management PowerShell module ..."
            Install-Module ExchangeOnlineManagement -Scope CurrentUser -AllowClobber -Force -ErrorAction Stop
        }
        catch {
            Add-EmptyLine
            Write-NCMessage "Can't install Exchange Online Management module. $($_.Exception.Message)" -Level ERROR
            return $false
        }
    }

    $explicitUpnRequested = $PSBoundParameters.ContainsKey('UserPrincipalName') -and -not [string]::IsNullOrWhiteSpace($UserPrincipalName)
    $resolvedUpn = if ($explicitUpnRequested) { $UserPrincipalName } else { $null }

    if (-not $ForceReconnect.IsPresent) {
        try {
            Get-EXOMailbox -ResultSize 1 -ErrorAction Stop | Out-Null

            $currentUpn = $null
            try {
                $currentUpn = (Get-ConnectionInformation -ErrorAction Stop | Select-Object -First 1).UserPrincipalName
            }
            catch {
                # Get-ConnectionInformation can throw (e.g. HttpResponseMessage/GetResponseHeader clash when
                # Microsoft.Graph is also loaded) even though the EXO session above is perfectly healthy.
                # Skip the UPN comparison rather than forcing an unnecessary reconnect.
            }

            if (-not $explicitUpnRequested) {
                # No explicit UPN was requested: keep whatever session is already active, whoever it belongs
                # to, instead of forcing a reconnect to the Windows AD identity (Find-UserConnected).
                return $true
            }

            if ([string]::IsNullOrWhiteSpace($currentUpn) -or $currentUpn -eq $resolvedUpn) {
                return $true
            }

            Write-NCMessage "Existing Exchange Online session is for $currentUpn, not $resolvedUpn. Reconnecting ..." -Level WARNING
        }
        catch {
            Write-NCMessage "Existing Exchange Online session not detected. Reconnecting ..." -Level WARNING
        }
    }

    if (-not $explicitUpnRequested -and [string]::IsNullOrWhiteSpace($resolvedUpn)) {
        $resolvedUpn = Find-UserConnected
    }

    $resolvedUpn = if ([string]::IsNullOrWhiteSpace($resolvedUpn)) { $null } else { $resolvedUpn }
    $message = if ($resolvedUpn) {
        "Connecting to Microsoft Exchange Online Management.`nUsing $resolvedUpn ..."
    }
    else {
        "Connecting to Microsoft Exchange Online Management ..."
    }
    Write-Verbose $message

    try {
        if ($resolvedUpn) {
            Connect-EOL -UserPrincipalName $resolvedUpn -DisableWAM:$DisableWAM.IsPresent -ErrorAction Stop
        }
        else {
            Connect-EOL -DisableWAM:$DisableWAM.IsPresent -ErrorAction Stop
        }

        # Run a lightweight cmdlet to be sure the session is alive.
        Get-EXOMailbox -ResultSize 1 -ErrorAction Stop | Out-Null
        return $true
    }
    catch {
        Add-EmptyLine
        Write-NCMessage "Unable to establish Exchange Online session. $($_.Exception.Message)" -Level ERROR
        return $false
    }
}

function Test-MgGraphConnection {
    <#
    .SYNOPSIS
        Ensures a Microsoft Graph session is available with the requested scopes.
    .DESCRIPTION
        Verifies that the Microsoft Graph module is present (installs it if requested),
        checks the current MgGraph context for the required scopes, and reconnects if needed.
        Optionally runs the Exchange Online prerequisite first to preserve legacy behaviour.
    .PARAMETER Scopes
        Delegated scopes to request during Connect-MgGraph. Defaults to User.Read.All.
    .PARAMETER TenantId
        Optional tenant to target explicitly.
    .PARAMETER UseDeviceCode
        Use device code authentication instead of opening a browser window.
    .PARAMETER AutoInstall
        Install Microsoft.Graph automatically if missing.
    .PARAMETER ForceReconnect
        Skip context validation and force a new Connect-MgGraph call.
    .PARAMETER EnsureExchangeOnline
        Run Test-EOLConnection before attempting Graph (defaults to true for backward compatibility).
    .PARAMETER LoginHint
        UPN to pass as -LoginHint to Connect-MgGraph, helping the WAM broker resolve the target
        account without prompting when it differs from the signed-in Windows account.
    #>

    [CmdletBinding()]
    param(
        [string[]]$Scopes = @('User.Read.All'),
        [string]$TenantId,
        [switch]$UseDeviceCode,
        [switch]$AutoInstall,
        [switch]$ForceReconnect,
        [bool]$EnsureExchangeOnline = $true,
        [string]$LoginHint
    )

    if ($EnsureExchangeOnline -and -not (Test-EOLConnection -AutoInstall:$AutoInstall.IsPresent)) {
        Write-NCMessage "Exchange Online prerequisite check failed. Skipping Microsoft Graph connection." -Level ERROR
        return $false
    }

    $requestedScopes = @($Scopes | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
    if (-not $requestedScopes) {
        $requestedScopes = @('User.Read.All')
    }

    $graphModuleAvailable = @(Get-Module -Name Microsoft.Graph -ListAvailable).Count -gt 0
    if (-not $graphModuleAvailable) {
        Write-NCMessage "Microsoft Graph PowerShell module is not available." -Level WARNING

        $installModule = $AutoInstall.IsPresent
        if (-not $installModule) {
            $confirmation = Read-Host "Install Microsoft Graph PowerShell module now? [Y] Yes [N] No"
            $installModule = $confirmation -match '^[yY]'
        }

        if (-not $installModule) {
            Add-EmptyLine
            Write-NCMessage "Microsoft Graph PowerShell module is required. Install it with Install-Module Microsoft.Graph." -Level ERROR
            return $false
        }

        try {
            Write-Verbose "Installing Microsoft Graph PowerShell module ..."
            Install-Module Microsoft.Graph -Scope CurrentUser -AllowClobber -Force -ErrorAction Stop
        }
        catch {
            Add-EmptyLine
            Write-NCMessage "Can't install Microsoft Graph module. $($_.Exception.Message)" -Level ERROR
            return $false
        }
    }

    try {
        Import-Module Microsoft.Graph.Authentication -ErrorAction Stop | Out-Null
    }
    catch {
        Add-EmptyLine
        Write-NCMessage "Unable to import Microsoft.Graph.Authentication. $($_.Exception.Message)" -Level ERROR
        return $false
    }

    $missingScopeMessage = {
        param($currentScopes, $requiredScopes)
        if (-not $requiredScopes -or $requiredScopes.Count -eq 0) { return $null }
        if (-not $currentScopes -or $currentScopes.Count -eq 0) { return $requiredScopes }
        $requiredScopes | Where-Object { $currentScopes -notcontains $_ }
    }

    if (-not $ForceReconnect.IsPresent) {
        try {
            $ctx = Get-MgContext -ErrorAction Stop
            if ($ctx -and $ctx.Account) {
                if ((-not [string]::IsNullOrWhiteSpace($LoginHint)) -and $ctx.Account -ne $LoginHint) {
                    Write-NCMessage "Existing Microsoft Graph session is for $($ctx.Account), not $LoginHint. Reconnecting ..." -Level WARNING
                }
                else {
                    $missingScopes = &$missingScopeMessage $ctx.Scopes $requestedScopes
                    if (-not $missingScopes -or $missingScopes.Count -eq 0) {
                        return $true
                    }

                    Write-NCMessage "Existing Microsoft Graph session missing required scopes ($($missingScopes -join ', ')). Reconnecting ..." -Level WARNING
                }
            }
            else {
                Write-Verbose "Microsoft Graph context not found. Establishing connection ..."
            }
        }
        catch {
            Write-NCMessage "Unable to retrieve current Microsoft Graph context. Reconnecting ..." -Level WARNING
        }
    }

    $scopeLabel = $requestedScopes -join ', '
    Write-Verbose "Connecting to Microsoft Graph requesting scopes: $scopeLabel"

    $connectParams = @{
        Scopes    = $requestedScopes
        NoWelcome = $true
    }

    if ($TenantId) {
        $connectParams.TenantId = $TenantId
    }

    if ($UseDeviceCode.IsPresent) {
        $connectParams.UseDeviceCode = $true
    }

    if (-not [string]::IsNullOrWhiteSpace($LoginHint)) {
        # -LoginHint only exists from Microsoft.Graph.Authentication 2.39.0
        $connectCommand = Get-Command Connect-MgGraph -ErrorAction SilentlyContinue
        if ($connectCommand -and $connectCommand.Parameters.ContainsKey('LoginHint')) {
            $connectParams.LoginHint = $LoginHint
        }
        else {
            Write-Verbose "Connect-MgGraph has no -LoginHint (added in Microsoft.Graph.Authentication 2.39.0); signing in without it."
        }
    }

    try {
        Connect-MgGraph @connectParams | Out-Null

        $ctx = Get-MgContext -ErrorAction Stop
        if (-not $ctx -or -not $ctx.Account) {
            throw "Connect-MgGraph did not return an authenticated context."
        }

        $missingScopes = &$missingScopeMessage $ctx.Scopes $requestedScopes
        if ($missingScopes -and $missingScopes.Count -gt 0) {
            throw "Connected session is missing scopes: $($missingScopes -join ', ')"
        }

        return $true
    }
    catch {
        Add-EmptyLine
        Write-NCMessage "Unable to establish Microsoft Graph session. $($_.Exception.Message)" -Level ERROR
        return $false
    }
}