Public/Connect-EMOS.ps1

function Connect-EMOS {
    <#
    .SYNOPSIS
        Connects to Microsoft Graph with the scopes required by EMOS.
    .DESCRIPTION
        Establishes a Microsoft Graph session using one of several auth flows.
        If a session already exists with sufficient scopes, it is reused.
 
        Supported auth flows:
          - Interactive : browser pop-up (default for human admins)
          - DeviceCode : for non-interactive / SSH terminals
          - Certificate : app-only via thumbprint or path (for CI/CD pipelines)
          - ClientSecret : app-only via secret (less secure; prefer certificate)
          - ManagedIdentity: system- or user-assigned Azure managed identity
 
    .PARAMETER TenantId
        Entra tenant ID or domain. Required for app-only flows.
    .PARAMETER ClientId
        App registration client ID. Required for app-only flows.
    .PARAMETER CertificateThumbprint
        Certificate thumbprint in the local machine/user store. App-only.
    .PARAMETER CertificatePath
        Path to a PFX certificate file. App-only.
    .PARAMETER ClientSecret
        App registration client secret (SecureString). App-only.
    .PARAMETER ManagedIdentity
        Connect using the Azure managed identity of the current host.
    .PARAMETER UseDeviceCode
        Use device-code flow instead of interactive browser.
    .PARAMETER Force
        Re-authenticate even if a valid session already exists.
    .EXAMPLE
        # Interactive (human admin, most common)
        Connect-EMOS
 
    .EXAMPLE
        # Device code (SSH / headless terminal)
        Connect-EMOS -TenantId "contoso.onmicrosoft.com" -UseDeviceCode
 
    .EXAMPLE
        # App-only with certificate (CI/CD recommended)
        Connect-EMOS -TenantId "contoso.onmicrosoft.com" -ClientId "abc..." -CertificateThumbprint "DEF123..."
 
    .EXAMPLE
        # App-only with client secret
        $secret = Read-Host "Client secret" -AsSecureString
        Connect-EMOS -TenantId "contoso.onmicrosoft.com" -ClientId "abc..." -ClientSecret $secret
 
    .EXAMPLE
        # Azure managed identity (Azure Automation, GitHub Actions with OIDC)
        Connect-EMOS -ManagedIdentity
    #>

    [CmdletBinding(DefaultParameterSetName = 'Interactive')]
    param(
        # --- App-only shared params ---
        [Parameter(ParameterSetName = 'Certificate',     Mandatory)]
        [Parameter(ParameterSetName = 'CertificatePath', Mandatory)]
        [Parameter(ParameterSetName = 'ClientSecret',    Mandatory)]
        [Parameter(ParameterSetName = 'Interactive')]
        [Parameter(ParameterSetName = 'DeviceCode')]
        [string]$TenantId,

        [Parameter(ParameterSetName = 'Certificate',     Mandatory)]
        [Parameter(ParameterSetName = 'CertificatePath', Mandatory)]
        [Parameter(ParameterSetName = 'ClientSecret',    Mandatory)]
        [string]$ClientId,

        # --- Auth flow params ---
        [Parameter(ParameterSetName = 'Certificate',     Mandatory)]
        [string]$CertificateThumbprint,

        [Parameter(ParameterSetName = 'CertificatePath', Mandatory)]
        [string]$CertificatePath,

        [Parameter(ParameterSetName = 'ClientSecret',    Mandatory)]
        [System.Security.SecureString]$ClientSecret,

        [Parameter(ParameterSetName = 'ManagedIdentity', Mandatory)]
        [switch]$ManagedIdentity,

        [Parameter(ParameterSetName = 'DeviceCode',      Mandatory)]
        [switch]$UseDeviceCode,

        [switch]$Force
    )

    $requiredScopes = @(
        'Group.Read.All'
        'AdministrativeUnit.Read.All'
        'EntitlementManagement.Read.All'
        'Policy.Read.All'
        'Application.Read.All'
    )

    # Reuse existing session if scopes are sufficient and -Force not set
    if (-not $Force) {
        $ctx = Get-MgContext -ErrorAction SilentlyContinue
        if ($ctx) {
            $missing = $requiredScopes | Where-Object { $_ -notin $ctx.Scopes }
            if (-not $missing) {
                Write-Host "Reusing existing Graph session: $($ctx.Account ?? $ctx.AppName) | Tenant: $($ctx.TenantId)" -ForegroundColor Green
                Write-Host "Days until MemberOf retirement: $([int](($script:EMOS_RETIREMENT_DATE - (Get-Date)).TotalDays))" -ForegroundColor Yellow
                return
            }
            Write-Verbose "Existing session missing scopes: $($missing -join ', '). Reconnecting..."
        }
    }

    Write-Host "Connecting to Microsoft Graph ($($PSCmdlet.ParameterSetName) flow)..." -ForegroundColor Cyan

    $connectParams = @{}
    if ($TenantId) { $connectParams['TenantId'] = $TenantId }

    switch ($PSCmdlet.ParameterSetName) {

        'Interactive' {
            $connectParams['Scopes'] = $requiredScopes
        }

        'DeviceCode' {
            $connectParams['Scopes']        = $requiredScopes
            $connectParams['UseDeviceCode'] = $true
        }

        'Certificate' {
            $connectParams['ClientId']              = $ClientId
            $connectParams['CertificateThumbprint'] = $CertificateThumbprint
            # App-only: no delegated scopes
        }

        'CertificatePath' {
            if (-not (Test-Path $CertificatePath)) {
                throw "Certificate file not found: $CertificatePath"
            }
            $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($CertificatePath)
            $connectParams['ClientId']    = $ClientId
            $connectParams['Certificate'] = $cert
        }

        'ClientSecret' {
            $connectParams['ClientId']              = $ClientId
            $connectParams['ClientSecretCredential'] = [System.Net.NetworkCredential]::new('', $ClientSecret)
        }

        'ManagedIdentity' {
            $connectParams['Identity'] = $true
        }
    }

    Connect-MgGraph @connectParams -NoWelcome

    $ctx = Get-MgContext
    $identity = $ctx.Account ?? $ctx.AppName ?? "Managed Identity"
    Write-Host "Connected as: $identity | Tenant: $($ctx.TenantId)" -ForegroundColor Green
    Write-Host "Days until MemberOf retirement: $([int](($script:EMOS_RETIREMENT_DATE - (Get-Date)).TotalDays))" -ForegroundColor Yellow
}

function Disconnect-EMOS {
    <#
    .SYNOPSIS
        Disconnects the current Microsoft Graph session.
    #>

    [CmdletBinding()]
    param()
    Disconnect-MgGraph -ErrorAction SilentlyContinue
    Write-Host "Disconnected from Microsoft Graph." -ForegroundColor Cyan
}