Public/Authentication/Connect-FleetDM.ps1

function Connect-FleetDM {
    <#
    .SYNOPSIS
        Establishes a connection to the FleetDM API
     
    .DESCRIPTION
        Connects to a FleetDM instance using either an API token or email/password credentials.
        The connection is stored at the module level and used by all other FleetDM cmdlets.
 
        The token is checked with GET /api/v1/fleet/version, then GET /api/v1/fleet/me.
        An API-only user can be limited to specific endpoints. If /version returns 403 and
        /me returns the current user (id and email), the connection is stored with Version
        set to $null. HTTP 401 is an authentication failure. HTTP 403 from /me means the
        token needs GET /api/v1/fleet/me allowed. Other FleetDM cmdlets do not read the
        stored version. Use -SkipVersionCheck to skip /version and require a successful /me call.
     
    .PARAMETER BaseUri
        The base URI of your FleetDM instance (e.g., https://fleet.example.com)
     
    .PARAMETER ApiToken
        A secure string containing the FleetDM API token.
        This is the preferred method for automation and API-only users.
     
    .PARAMETER Credential
        PSCredential object containing email and password for FleetDM authentication.
        Use this for interactive sessions or when token authentication is not available.
 
    .PARAMETER Email
        Fleet user email. Same value as fleetctl login --email.
 
    .PARAMETER Password
        Fleet user password. Same value as fleetctl login --password.
 
    .PARAMETER SkipVersionCheck
        Do not call GET /api/v1/fleet/version. The token is checked with GET /api/v1/fleet/me,
        and the stored version is $null. Use this when an API-only user is not allowed to call
        the version endpoint.
     
    .EXAMPLE
        $token = ConvertTo-SecureString "your-api-token" -AsPlainText -Force
        Connect-FleetDM -BaseUri "https://fleet.example.com" -ApiToken $token
         
        Connects to FleetDM using an API token
 
    .EXAMPLE
        $password = Read-Host "Password" -AsSecureString
        Connect-FleetDM -BaseUri "https://fleet.example.com" -Email "admin@example.com" -Password $password
 
        Logs in with email and password, the same call as fleetctl login --email --password
     
    .EXAMPLE
        $cred = Get-Credential
        Connect-FleetDM -BaseUri "https://fleet.example.com" -Credential $cred
         
        Prompts for credentials and connects to FleetDM
     
    .EXAMPLE
        Connect-FleetDM -BaseUri "https://fleet.example.com" -ApiToken (Read-Host -AsSecureString "Enter API Token")
         
        Prompts for API token securely and connects to FleetDM
 
    .EXAMPLE
        $token = ConvertTo-SecureString "your-api-token" -AsPlainText -Force
        Connect-FleetDM -BaseUri "https://fleet.example.com" -ApiToken $token -SkipVersionCheck
 
        Connects when the token can call /me but is not allowed to call /version.
        The connection version is $null.
     
    .LINK
        https://fleetdm.com/docs/using-fleet/rest-api#authentication
    #>

    [CmdletBinding(DefaultParameterSetName = 'Token')]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [ValidatePattern('^https?://')]
        [string]$BaseUri,
        
        [Parameter(Mandatory, ParameterSetName = 'Token')]
        [securestring]$ApiToken,
        
        [Parameter(Mandatory, ParameterSetName = 'Credential')]
        [pscredential]$Credential,

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

        [Parameter(Mandatory, ParameterSetName = 'Email')]
        [securestring]$Password,

        [Parameter()]
        [switch]$SkipVersionCheck
    )
    
    begin {
        # Remove trailing slash from BaseUri if present
        $BaseUri = $BaseUri.TrimEnd('/')
        
        # Initialize web session for connection pooling
        $script:FleetDMWebSession = New-Object Microsoft.PowerShell.Commands.WebRequestSession
    }
    
    process {
        try {
            if ($PSCmdlet.ParameterSetName -eq 'Email') {
                $Credential = [pscredential]::new($Email, $Password)
            }

            $headers = @{
                'Content-Type' = 'application/json'
            }
            
            if ($PSCmdlet.ParameterSetName -eq 'Token') {
                # Convert secure string to plain text - use PtrToStringBSTR for cross-platform compatibility
                $tokenBstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($ApiToken)
                $tokenPlain = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($tokenBstr)
                
                # Clean up the BSTR
                [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($tokenBstr)
                
                $headers['Authorization'] = "Bearer $tokenPlain"
                
                Write-Verbose "Attempting token-based authentication to $BaseUri"
            }
            else {
                # Login with credentials
                Write-Verbose "Attempting credential-based authentication to $BaseUri"
                
                $loginBody = @{
                    email = $Credential.UserName
                    password = $Credential.GetNetworkCredential().Password
                } | ConvertTo-Json
                
                $loginUri = "$BaseUri/api/v1/fleet/login"
                
                try {
                    $loginResponse = Invoke-RestMethod -Uri $loginUri `
                        -Method POST `
                        -Body $loginBody `
                        -ContentType 'application/json' `
                        -WebSession $script:FleetDMWebSession `
                        -ErrorAction Stop
                    
                    if (-not $loginResponse.token) {
                        throw "No token received from FleetDM login endpoint"
                    }
                    
                    $headers['Authorization'] = "Bearer $($loginResponse.token)"
                    
                    Write-Verbose "Successfully authenticated as $($loginResponse.user.email)"
                }
                catch {
                    throw "Failed to authenticate with FleetDM: $($_.Exception.Message)"
                }
            }
            
            # Version is display metadata. No other cmdlet reads it, so $null is safe.
            $fleetVersion = $null
            $versionAccepted = $false

            if ($SkipVersionCheck) {
                Write-Verbose "Skipping GET /api/v1/fleet/version. The token will be checked with GET /api/v1/fleet/me."
            }
            else {
                # Test connection by getting version
                $versionUri = "$BaseUri/api/v1/fleet/version"
                
                Write-Verbose "Testing connection to: $versionUri"
                Write-Verbose "Headers: $(Get-FleetRedactedLogText -Text $headers)"
                
                try {
                    $webResponse = Invoke-WebRequest -Uri $versionUri `
                        -Headers $headers `
                        -WebSession $script:FleetDMWebSession `
                        -ErrorAction Stop
                    
                    # Parse response based on content type
                    if ($webResponse.Headers['Content-Type'] -like 'application/json*') {
                        $versionResponse = $webResponse.Content | ConvertFrom-Json
                    }
                    else {
                        # Try to parse as JSON anyway
                        try {
                            $versionResponse = $webResponse.Content | ConvertFrom-Json
                        }
                        catch {
                            throw "Unexpected response format from version endpoint"
                        }
                    }

                    $fleetVersion = $versionResponse.version
                    $versionAccepted = $true
                    
                    Write-Verbose "Successfully connected to FleetDM version $fleetVersion"
                }
                catch {
                    Write-Verbose "Error details: $($_.Exception.Message)"
                    Write-Verbose "Error type: $($_.Exception.GetType().FullName)"

                    $statusCode = Get-FleetConnectStatusCode -ErrorRecord $_
                    if ($statusCode -eq 401) {
                        throw "Authentication failed. Please check your credentials or API token."
                    }
                    if ($statusCode -eq 403) {
                        Write-Verbose "GET /api/v1/fleet/version returned 403. The token will be checked with GET /api/v1/fleet/me. Version will be stored as null."
                        $fleetVersion = $null
                    }
                    else {
                        throw "Failed to connect to FleetDM: $($_.Exception.Message)"
                    }
                }
            }
            
            # Get current user information. This is the connection check when version
            # was skipped or returned 403.
            $meUri = "$BaseUri/api/v1/fleet/me"
            
            try {
                $userInfo = Invoke-RestMethod -Uri $meUri `
                    -Headers $headers `
                    -WebSession $script:FleetDMWebSession `
                    -ErrorAction Stop
            }
            catch {
                Write-Verbose "Error details: $($_.Exception.Message)"
                Write-Verbose "Error type: $($_.Exception.GetType().FullName)"

                if ($versionAccepted) {
                    Write-Warning "Could not retrieve user information"
                    $userInfo = $null
                }
                else {
                    $statusCode = Get-FleetConnectStatusCode -ErrorRecord $_
                    if ($statusCode -eq 401) {
                        throw "Authentication failed. Please check your credentials or API token."
                    }
                    if ($statusCode -eq 403) {
                        throw "Failed to connect to FleetDM: GET /api/v1/fleet/me returned 403. The token needs GET /api/v1/fleet/me allowed."
                    }

                    throw "Failed to connect to FleetDM: $($_.Exception.Message)"
                }
            }

            if (-not $versionAccepted -and -not (Test-FleetCurrentUser -UserInfo $userInfo)) {
                throw "Failed to connect to FleetDM: GET /api/v1/fleet/me did not return the current user."
            }

            if ($null -ne $userInfo -and $userInfo -isnot [string]) {
                Write-Verbose "Authenticated as: $($userInfo.user.name) ($($userInfo.user.email))"
                Write-Verbose "Global role: $($userInfo.user.global_role)"
            }
            
            # Store connection information
            $script:FleetDMConnection = @{
                BaseUri = $BaseUri
                Headers = $headers
                Version = $fleetVersion
                User = $userInfo.user
                ConnectedAt = Get-Date
            }
            
            # Create output object
            $connectionInfo = [PSCustomObject]@{
                PSTypeName = 'FleetDM.Connection'
                BaseUri = $BaseUri
                Version = $fleetVersion
                User = if ($userInfo) { $userInfo.user.email } else { 'Unknown' }
                GlobalRole = if ($userInfo) { $userInfo.user.global_role } else { 'Unknown' }
                ConnectedAt = $script:FleetDMConnection.ConnectedAt
            }
            
            Write-Host "Successfully connected to FleetDM at $BaseUri" -ForegroundColor Green
            if (-not [string]::IsNullOrWhiteSpace([string]$fleetVersion)) {
                Write-Host "Version: $fleetVersion" -ForegroundColor Cyan
            }
            if ($userInfo) {
                Write-Host "Authenticated as: $($userInfo.user.email) [$($userInfo.user.global_role)]" -ForegroundColor Cyan
            }
            
            return $connectionInfo
        }
        catch {
            # Clear any partial connection data
            $script:FleetDMConnection = $null
            $script:FleetDMWebSession = $null
            
            throw $_
        }
    }
}