Modules/M365DSCConnection.psm1

[hashtable]$Script:M365DSCTelemetryConnectionToGraphParams = @{}

<#
.SYNOPSIS
    Gets all resources that support the specified authentication method and determines the most secure authentication method supported by the resource.
 
.DESCRIPTION
    This function gets all resources that support the specified authentication method and
    determines the most secure authentication method supported by the resource.
 
.PARAMETER AuthenticationMethod
    Specifies the authentication method to check for. Valid values are:
    'ApplicationWithSecret', 'CertificateThumbprint', 'CertificatePath', 'Credentials',
    'CredentialsWithTenantId', 'CredentialsWithApplicationId', 'ManagedIdentity', 'AccessTokens'.
 
.PARAMETER Resources
    Specifies the resources to check. If not specified, all resources will be checked.
 
.FUNCTIONALITY
    Internal
#>

function Get-M365DSCComponentsWithMostSecureAuthenticationType
{
    [CmdletBinding()]
    [OutputType([System.Collections.Generic.List[System.Collections.Hashtable]])]
    param
    (
        [Parameter()]
        [System.String[]]
        [ValidateSet('ApplicationWithSecret', 'CertificateThumbprint', 'CertificatePath', 'Credentials', 'CredentialsWithTenantId', 'CredentialsWithApplicationId', 'ManagedIdentity', 'AccessTokens')]
        $AuthenticationMethod,

        [Parameter()]
        [System.String[]]
        $Resources
    )

    Initialize-M365DSCDllLoader -ErrorAction Stop

    $dscResourcesPath = Join-Path -Path $PSScriptRoot -ChildPath '../DscResources'
    return [Microsoft365DSC.Connection.ConnectionHelper]::GetComponentsWithMostSecureAuthenticationType(
        $dscResourcesPath,
        $AuthenticationMethod,
        $Resources
    )
}

<#
.SYNOPSIS
    Creates a new connection to the specified M365 workload.
 
.DESCRIPTION
    This function creates a new connection to the specified M365 workload
 
.PARAMETER Workload
    Specifies the M365 workload to connect to. Valid values are:
    'AdminAPI', 'Azure', 'AzureDevOPS', 'DefenderForEndpoint', 'EngageHub', 'ExchangeOnline',
    'Fabric', 'Licensing', 'SecurityComplianceCenter', 'PnP', 'PowerPlatforms',
    'PowerPlatformREST', 'MicrosoftTeams', 'MicrosoftGraph', 'SharePointOnlineREST', 'Tasks'.
 
.PARAMETER InboundParameters
    Specifies a hashtable of parameters to use for the connection. The keys and values in the hashtable should match the parameters of the Connect-M365Tenant function.
 
.PARAMETER Url
    Specifies the URL to use for the connection. This parameter is optional and can be used to override the default URL for the specified workload.
 
.PARAMETER EnableSearchOnlySession
    Specifies whether to enable a search-only session for the connection. This parameter is optional and can be used to limit the connection to read-only operations
    for the SecurityComplianceCenter workload.
 
.FUNCTIONALITY
    Internal
#>

function New-M365DSCConnection
{
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateSet('AdminAPI', 'Azure', 'AzureDevOPS', 'DefenderForEndpoint', 'EngageHub', 'ExchangeOnline', 'Fabric', 'Licensing', `
                'SecurityComplianceCenter', 'PnP', 'PowerPlatforms', 'PowerPlatformREST', `
                'MicrosoftTeams', 'MicrosoftGraph', 'SharePointOnlineREST', 'Tasks')]
        [System.String]
        $Workload,

        [Parameter(Mandatory = $true)]
        [ValidateScript({
                if ($null -ne $_.Credential)
                {
                    $invalid = $_.Credential.Username -notmatch '.onmicrosoft.'
                    if (-not $invalid)
                    {
                        return $true
                    }
                    else
                    {
                        Write-Warning -Message 'We recommend providing the username in the format of <tenant>.onmicrosoft.* for the Credential property.'
                    }
                }

                if ($null -ne $_.TenantId)
                {
                    $isValid = [System.Guid]::TryParse($_.TenantId, [ref][System.Guid]::Empty)
                    if ($isValid)
                    {
                        throw 'Please provide the tenant name (e.g., contoso.onmicrosoft.com) for TenantId instead of its GUID.'
                    }

                    $isValid = $_.TenantId -match '.onmicrosoft.'
                    if ($isValid)
                    {
                        return $true
                    }
                    else
                    {
                        Write-Warning -Message 'We recommend providing the tenant name in format <tenant>.onmicrosoft.* for TenantId.'
                    }
                }
                return $true
            })]
        [System.Collections.Hashtable]
        $InboundParameters,

        [Parameter()]
        [System.String]
        $Url,

        [Parameter()]
        [switch]
        $EnableSearchOnlySession
    )

    if (-not (Test-IsM365DSCRequiredModulesLoaded))
    {
        $requiredModules = Get-M365DSCRequiredModules
        foreach ($requiredModule in $requiredModules)
        {
            Write-Verbose -Message "Ensuring required module '$requiredModule' is loaded."
            Confirm-M365DSCLoadedModule -ModuleName $requiredModule
        }
        Set-M365DSCRequiredModulesLoaded -Value $true
    }

    if ($Workload -eq 'MicrosoftTeams')
    {
        try
        {
            $null = Get-Command 'Connect-MicrosoftTeams' -ErrorAction Stop
        }
        catch
        {
            Import-Module 'MicrosoftTeams' -Global -Force -Alias @() -Cmdlet @() -Variable @() -DisableNameChecking | Out-Null
        }
    }

    Write-Verbose -Message "Attempting connection to {$Workload} with:"
    Write-Verbose -Message "$($InboundParameters | Out-String)"

    #region Telemetry
    $data = [System.Collections.Generic.Dictionary[[System.String], [System.Object]]]::new()
    $data.Add('Source', 'M365DSCUtil')
    $data.Add('Workload', $Workload)

    $Script:M365DSCTelemetryConnectionToGraphParams = @{}

    # Keep track of workloads we already connected so that we don't send additional Telemetry events.
    if ($null -eq $Script:M365ConnectedToWorkloads)
    {
        Write-Verbose -Message 'Initializing the Connected To Workloads List.'
        $Script:M365ConnectedToWorkloads = @()
    }

    # Convert ApplicationSecret from SecureString to plain string.
    if ($InboundParameters.ApplicationSecret)
    {
        $InboundParameters.ApplicationSecret = $InboundParameters.ApplicationSecret.GetNetworkCredential().Password
    }

    #region Validation
    if ($null -ne $InboundParameters.Credential -and `
            -not [System.String]::IsNullOrEmpty($InboundParameters.CertificateThumbprint))
    {
        $message = 'Both Authentication methods are attempted'
        Write-Verbose -Message $message
        $data.Add('Exception', $message)
        $errorText = "You can't specify both the Credential and CertificateThumbprint"
        $data.Add('CustomMessage', $errorText)
        Add-M365DSCTelemetryEvent -Type 'Error' -Data $data
        throw $errorText
    }

    if ($null -eq $InboundParameters.Credential -and `
            [System.String]::IsNullOrEmpty($InboundParameters.ApplicationId) -and `
            [System.String]::IsNullOrEmpty($InboundParameters.TenantId) -and `
            [System.String]::IsNullOrEmpty($InboundParameters.CertificateThumbprint) -and `
            -not $InboundParameters.ManagedIdentity -and `
            $null -eq $InboundParameters.AccessTokens)
    {
        $message = 'No Authentication method was provided'
        Write-Verbose -Message $message
        $message += "`r`nProvided Keys --> $($InboundParameters.Keys)"
        $data.Add('Exception', $message)
        $errorText = 'You must specify either the Credential or ApplicationId, TenantId and CertificateThumbprint parameters.'
        $data.Add('CustomMessage', $errorText)
        Add-M365DSCTelemetryEvent -Type 'Error' -Data $data
        throw $errorText
    }
    #endregion Validation

    # Determine connection mode using the shared helper.
    $connectionMode = Get-M365DSCAuthenticationMode -Parameters $InboundParameters
    if ($connectionMode -eq 'Interactive')
    {
        throw 'Could not determine authentication method'
    }
    Write-Verbose -Message "Connecting via $connectionMode"

    #region Build Connect-M365Tenant splat
    $connectParams = @{
        Workload                = $Workload
        EnableSearchOnlySession = $EnableSearchOnlySession.IsPresent
    }

    if (-not [System.String]::IsNullOrEmpty($Url))
    {
        $connectParams.Url = $Url
    }

    if ($Workload -eq 'Azure' -and -not [System.String]::IsNullOrEmpty($InboundParameters.SubscriptionId))
    {
        $connectParams.SubscriptionId = $InboundParameters.SubscriptionId
    }

    switch ($connectionMode)
    {
        'Credentials'
        {
            $connectParams.Credential = $InboundParameters.Credential
        }
        'CredentialsWithApplicationId'
        {
            $connectParams.ApplicationId = $InboundParameters.ApplicationId
            $connectParams.Credential = $InboundParameters.Credential
        }
        'CredentialsWithTenantId'
        {
            $connectParams.TenantId = $InboundParameters.TenantId
            $connectParams.Credential = $InboundParameters.Credential
        }
        'ServicePrincipalWithPath'
        {
            $connectParams.ApplicationId = $InboundParameters.ApplicationId
            $connectParams.TenantId = $InboundParameters.TenantId
            $connectParams.CertificatePassword = $InboundParameters.CertificatePassword.Password
            $connectParams.CertificatePath = $InboundParameters.CertificatePath
        }
        'ServicePrincipalWithSecret'
        {
            $connectParams.ApplicationId = $InboundParameters.ApplicationId
            $connectParams.TenantId = $InboundParameters.TenantId
            $connectParams.ApplicationSecret = $InboundParameters.ApplicationSecret
        }
        'ServicePrincipalWithThumbprint'
        {
            $connectParams.ApplicationId = $InboundParameters.ApplicationId
            $connectParams.TenantId = $InboundParameters.TenantId
            $connectParams.CertificateThumbprint = $InboundParameters.CertificateThumbprint
        }
        'ManagedIdentity'
        {
            $connectParams.Identity = $true
            $connectParams.TenantId = $InboundParameters.TenantId
        }
        'AccessTokens'
        {
            $connectParams.AccessTokens = $InboundParameters.AccessTokens
            $connectParams.TenantId = $InboundParameters.TenantId
        }
    }
    #endregion

    Connect-M365Tenant @connectParams

    #region Update telemetry cache
    $telemetryCacheKeys = switch ($connectionMode)
    {
        'Credentials'                    { @('Credential') }
        'CredentialsWithApplicationId'   { @('Credential', 'ApplicationId') }
        'CredentialsWithTenantId'        { @('Credential', 'TenantId') }
        'ServicePrincipalWithPath'       { @('ApplicationId', 'TenantId', 'CertificatePath') }
        'ServicePrincipalWithSecret'     { @('ApplicationId', 'TenantId', 'ApplicationSecret') }
        'ServicePrincipalWithThumbprint' { @('ApplicationId', 'TenantId', 'CertificateThumbprint') }
        'ManagedIdentity'                { @('TenantId') }
        'AccessTokens'                   { @('AccessTokens', 'TenantId') }
    }

    foreach ($key in $telemetryCacheKeys)
    {
        if (-not $Script:M365DSCTelemetryConnectionToGraphParams.ContainsKey($key) -and
            $null -ne $InboundParameters[$key])
        {
            $Script:M365DSCTelemetryConnectionToGraphParams.Add($key, $InboundParameters[$key])
        }
    }

    # Handle special telemetry cache values not directly from InboundParameters.
    if ($connectionMode -eq 'ManagedIdentity' -and
        -not $Script:M365DSCTelemetryConnectionToGraphParams.ContainsKey('Identity'))
    {
        $Script:M365DSCTelemetryConnectionToGraphParams.Add('Identity', $true)
    }
    if ($connectionMode -eq 'ServicePrincipalWithPath' -and
        -not $Script:M365DSCTelemetryConnectionToGraphParams.ContainsKey('CertificatePassword'))
    {
        $Script:M365DSCTelemetryConnectionToGraphParams.Add('CertificatePassword', $InboundParameters.CertificatePassword.Password)
    }
    #endregion

    #region Emit connection telemetry
    # The Credentials mode uses 'Credential' (no trailing 's') as tracking key for backward compatibility.
    $trackingKey = if ($connectionMode -eq 'Credentials') { 'Credential' } else { $connectionMode }
    $workloadTrackingKey = "$Workload-$trackingKey"

    if (-not ($Script:M365ConnectedToWorkloads -contains $workloadTrackingKey))
    {
        $data.Add('ConnectionMode', $connectionMode)

        if (-not $data.ContainsKey('Tenant'))
        {
            if (-not [System.String]::IsNullOrEmpty($InboundParameters.TenantId))
            {
                $data.Add('Tenant', $InboundParameters.TenantId)
            }
            elseif ($null -ne $InboundParameters.Credential)
            {
                try
                {
                    $tenantId = $InboundParameters.Credential.Username.Split('@')[1]
                    $data.Add('Tenant', $tenantId)
                    if (-not $Script:M365DSCTelemetryConnectionToGraphParams.ContainsKey('TenantId'))
                    {
                        $Script:M365DSCTelemetryConnectionToGraphParams.Add('TenantId', $tenantId)
                    }
                }
                catch
                {
                    Write-Verbose -Message $_
                }
            }
        }

        Add-M365DSCTelemetryEvent -Data $data -Type 'Connection'
        $Script:M365ConnectedToWorkloads += $workloadTrackingKey
    }
    #endregion

    return $connectionMode
}

<#
.SYNOPSIS
    Gets the authentication mode based on the specified parameters.
 
.DESCRIPTION
    This function gets the used authentication mode based on the specified parameters
 
.PARAMETER Parameters
    Specifies a hashtable of parameters to use for determining the authentication mode. The keys and values in the hashtable should match the parameters of the Connect-M365Tenant function.
 
.FUNCTIONALITY
    Internal
#>

function Get-M365DSCAuthenticationMode
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.Collections.Hashtable]
        $Parameters
    )

    # Cache frequently accessed values to reduce hashtable lookups
    $applicationId = $Parameters.ApplicationId
    $tenantId = $Parameters.TenantId
    $credential = $Parameters.Credential

    # Check service principal authentication modes first (most common in automation)
    if ($applicationId -and $tenantId)
    {
        if ($Parameters.CertificateThumbprint)
        {
            return 'ServicePrincipalWithThumbprint'
        }
        if ($Parameters.ApplicationSecret)
        {
            return 'ServicePrincipalWithSecret'
        }
        if ($Parameters.CertificatePath -and $Parameters.CertificatePassword)
        {
            return 'ServicePrincipalWithPath'
        }
    }

    # Check credential-based authentication
    if ($credential)
    {
        if ($applicationId)
        {
            return 'CredentialsWithApplicationId'
        }
        if ($tenantId)
        {
            return 'CredentialsWithTenantId'
        }
        return 'Credentials'
    }

    # Check other authentication modes
    if ($Parameters.ManagedIdentity)
    {
        return 'ManagedIdentity'
    }

    if ($Parameters.AccessTokens)
    {
        return 'AccessTokens'
    }

    # Default to interactive
    return 'Interactive'
}

<#
.SYNOPSIS
    Retrieves the telemetry connection parameters for the current session.
 
.DESCRIPTION
    This function retrieves the telemetry connection parameters for the current session.
 
.FUNCTIONALITY
    Internal.
#>

function Get-M365DSCTelemetryConnectionParameter
{
    [CmdletBinding()]
    param ()

    $Script:M365DSCTelemetryConnectionToGraphParams.Clone()
}

<#
.SYNOPSIS
    Sets the telemetry connection parameters for the current session.
 
.DESCRIPTION
    This function sets the telemetry connection parameters for the current session.
 
.PARAMETER Parameters
    Specifies a hashtable of parameters to set for the telemetry connection. The keys and values in the hashtable should match the parameters of the Connect-M365Tenant function.
 
.FUNCTIONALITY
    Internal.
#>

function Set-M365DSCTelemetryConnectionParameter
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [hashtable]$Parameters
    )

    $Script:M365DSCTelemetryConnectionToGraphParams = $Parameters.Clone()
}

Export-ModuleMember -Function @(
    'Get-M365DSCAuthenticationMode',
    'Get-M365DSCComponentsWithMostSecureAuthenticationType',
    'Get-M365DSCFunctionParameterNamesByAST',
    'Get-M365DSCTelemetryConnectionParameter',
    'New-M365DSCConnection',
    'Set-M365DSCTelemetryConnectionParameter'
)