Public/Authentication.ps1

## Ensure moveSessions folder exists
$moveSessionsFolderPath = Join-Path -Path $userPaths.credentials -ChildPath '.moveSessions'
if (-not (Test-Path -Path $moveSessionsFolderPath -PathType Container)) {
    $null = New-Item -Path $moveSessionsFolderPath -ItemType Directory
}

## Define a MoveSessions Variable to store connection details in
New-Variable -Scope Global -Name 'MoveSessions' -Value @{ } -ErrorAction SilentlyContinue
New-Variable -Scope Global -Name 'MoveSessionsPath' -Value (Join-Path -Path $userPaths.credentials -ChildPath '.moveSessions') -ErrorAction SilentlyContinue

function New-MoveSession {
    <#
    .SYNOPSIS
    Creates a connection to a Move VM instance
 
    .DESCRIPTION
    This function creates a connection to the specified Move instance using
    the provided credentials. This session can be used when calling other functions within
    the Move module
 
    .PARAMETER SessionName
    The name that will be used when referring to the created MoveSession
 
    .PARAMETER Server
    The URI of the Move instance to connect to
 
    .PARAMETER Credential
    The credentials to be used twhen connecting to Move
 
    .PARAMETER AllowInsecureSSL
    Switch indicating whether or not an insecure SSL connection is allowed
 
    .EXAMPLE
    $Session = @{
        SessionName = 'TMDDEV'
        Server = 'tmddev.Move.net'
        Credential = (Get-StoredCredential -Name 'ME')
    }
    New-MoveSession @Session
 
    .OUTPUTS
    None
    #>


    [CmdletBinding()]
    [Alias('Connect-MoveServer')]
    param(
        [Parameter(
            Mandatory = $true,
            ValueFromPipelineByPropertyName = $true)]
        [string]$Server,

        [Parameter(
            Mandatory = $false,
            ValueFromPipelineByPropertyName = $true)]
        [int]$Port = 443,

        [Parameter(
            Mandatory = $true,
            ValueFromPipelineByPropertyName = $true)]
        [pscredential]$Credential,

        [Parameter(
            Mandatory = $false,
            ValueFromPipelineByPropertyName = $true)]
        [switch]$AllowInsecureSSL,

        [Parameter(
            DontShow = $true,
            Mandatory = $false)]
        [switch]$Force,

        [Parameter(
            Mandatory = $false)]
        [switch]$Passthru
    )

    begin {
        $SessionName = 'Default'
        # If there is no saved session, build the path to the one we will create
        if (-not $Force.IsPresent) {
            try {
                $savedSession = Get-MoveSession -Server $Server
            } catch {
                Write-Error "No Move Session found for [$Server]."
            }
        }
    }

    process {

        if ($savedSession) {
            Write-Verbose 'Returning existing session'
            $Global:MoveSessions[$savedSession.Name] = $savedSession
            return $Passthru.IsPresent ? $savedSession : $null
        }

        ## Create a session object for this new connection
        $NewMoveSession = [MoveSession]::new($SessionName, $Server, $AllowInsecureSSL)

        ## Trim the server name
        $Instance = $Server -replace '^https?://'

        ## Save the Instance and Port used
        $NewMoveSession.MoveServer = $Instance

        ## Prepare Request Headers for use in the Session Header Cache
        $ContentType = 'application/json;charset=UTF-8'

        # Authenticating with Move APIs - Basic AUTH over SSL
        $RequestHeaders = @{
            'Content-Type'  = $ContentType
            'Accept'        = 'application/json'
            'Cache-Control' = 'no-cache'
        }

        $body = @{
            Spec = @{
                Username = $Credential.UserName
                Password = $Credential.GetNetworkCredential().Password
            }
        }
        $WebRequestSplat = @{
            Method                          = 'POST'
            Uri                             = 'https://{0}:{1}/move/v2/users/login' -f $Instance, $Port
            Headers                         = $RequestHeaders
            SessionVariable                 = 'MoveWebSession'
            PreserveAuthorizationOnRedirect = $true
            ContentType                     = $ContentType
            Body                            = $body | ConvertTo-Json -Compress
            ProgressAction                  = 'SilentlyContinue'
            SkipCertificateCheck            = $AllowInsecureSSL.IsPresent
        }

        ## Attempt Login
        if ($VerbosePreference -eq 'Continue') {
            Write-Host 'Logging into Move instance [ ' -NoNewline
            Write-Host $Instance -ForegroundColor Cyan -NoNewline
            Write-Host ' ] as [ ' -NoNewline
            Write-Host $Credential.UserName -ForegroundColor Cyan -NoNewline
            Write-Host ' ]'
        }

        # Make the request
        try {
            Write-Verbose 'Invoking web request'
            $Response = Invoke-WebRequest @WebRequestSplat
            ## Check the Response code for 200
            if ($Response.StatusCode -eq 200) {
                $ResponseContent = $Response.Content | ConvertFrom-Json
            }
        } catch {
            Write-Host $_.Exception.Message
            Write-Host $_.Exception.InnerException.Message
            throw $_
        }

        ## Add this Session to the MoveSessions list and save it to disk
        Write-Verbose 'Populating NewMoveSession'
        $NewMoveSession.MoveSessionRefreshData = $Credential
        $MoveWebSession.Headers['Authorization'] = $ResponseContent.Status.Token
        $NewMoveSession.ExpirationDate = [datetime]::UnixEpoch.AddSeconds($ResponseContent.Status.ExpiryDate)
        $NewMoveSession.MoveWebSession = $MoveWebSession
        $sessionPath = Join-Path -Path $MoveSessionsPath -ChildPath 'Default.json' -ErrorAction Stop
        $NewMoveSession | ConvertTo-Json -Depth 10 | Out-File -FilePath $sessionPath -Force
        Write-Verbose 'Exported NewMoveSession, adding to MoveSessions'
        $Global:MoveSessions[$SessionName] = $NewMoveSession

        ## Return the session if requested
        if ($Passthru.IsPresent) {
            return $NewMoveSession
        }
    }
}

function Get-MoveSession {
    <#
    .SYNOPSIS
    Gets a MoveSession by Name, Server or Version
 
    .PARAMETER Name
    One or more MoveSession names to get
 
    .PARAMETER Server
    One or more TM servers for which a MoveSession has been created
 
    .PARAMETER Version
    One or more TM server versions for which a MoveSession has been created
 
    .EXAMPLE
    Get-MoveSession -Name 'Default', 'DEV'
 
    .EXAMPLE
    Get-MoveSession -Version '6.1.*'
 
    .EXAMPLE
    Get-MoveSession -Server '*.Move.net'
 
    .OUTPUTS
    One MoveSession, or an array of MoveSessions depending on the number of sessions to return
    #>


    [CmdletBinding(DefaultParameterSetName = 'ByName')]
    param(
        [Parameter(
            Mandatory = $false,
            Position = 0,
            ParameterSetName = 'ByName',
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true)]
        [Alias('SessionName')]
        [string[]]$Name = '*',

        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'ByServer',
            ValueFromPipelineByPropertyName = $true)]
        [Parameter(
            Mandatory = $false,
            ValueFromPipelineByPropertyName = $true)]
        [string]$Server,

        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'ByVersion',
            ValueFromPipelineByPropertyName = $true)]
        [string[]]$Version,

        [Parameter(
            Mandatory = $false,
            Position = 0,
            ParameterSetName = 'ByObject')]
        [MoveSession]$MoveSession
    )

    begin {
        # Is there a valid session in the Global:MoveSessions variable?}
        if (Test-MoveSession -MoveSession $Global:MoveSessions.Default) {
            Write-Verbose 'Returning valid session from Global var'
            $existingSession = $Global:MoveSessions.Default
            return # go to the process block
        } else {
            Write-Verbose 'No valid sessions in Global var'
            $credentialFromExpiredSession = $Global:MoveSessions.Default.MoveSessionRefreshData
            $Global:MoveSessions.Clear()
        }

        # Is there a valid session saved to disk?
        $message = 'Checking for a saved session:'
        switch ($PSCmdlet.ParameterSetName) {
            'ByObject' {
                $message = "$message MoveSession Server = {0}" -f $MoveSession.MoveServer
            }
            'ByServer' {
                $message = "$message Server = {0}" -f $Server
            }
            default {
                $message = "$message Name = {0}" -f $Name
            }
        }
        Write-Verbose $message
        $sessionPath = Join-Path -Path $MoveSessionsPath -ChildPath 'Default.json'

        try {
            $existingSessionJson = Get-Content -Path $sessionPath -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
            $existingSession = [MoveSession]::new(
                'Default', $existingSessionJson.MoveServer, $existingSessionJson.MovePort, $existingSessionJson.AllowInsecureSSL
            )
            $existingWebSession = [Microsoft.PowerShell.Commands.WebRequestSession]::new()
            # $existingWebSession.Headers = $existingSessionJson.MoveWebSession.Headers
            foreach ($header in $existingSessionJson.MoveWebSession.Headers.psobject.Members | Where-Object membertype -EQ NoteProperty) {
                $existingWebSession.Headers.Add($header.Name, $header.Value)
            }
            $existingSession.MoveWebSession = $existingWebSession
            $cookies = [System.Net.CookieContainer]::new()
            $existingSessionJson.MoveWebSession.Cookies = $cookies

            if (Test-MoveSession -MoveSession $existingSession) {
                $Global:MoveSessions.Default = $existingSession
                Write-Verbose "Returning existing MoveSession from path: $sessionPath"
            } else {
                # there is no refresh token
                # create a new session

                try {
                    Write-Verbose 'Creating New Session'
                    # New-MoveSession -Server $existingSession.MoveServer -Port $existingSession.MovePort -Credential $existingSession.Credential
                    $newSessionSplat = @{
                        Server           = $Global:MoveSessions.Default.MoveServer ?? $existingSession.MoveServer
                        Port             = $Global:MoveSessions.Default.MovePort ?? $existingSession.MovePort
                        Credential       = $Global:MoveSessions.Default.MoveSessionRefreshData ?? $existingSession.MoveSessionRefreshData ?? $credentialFromExpiredSession
                        AllowInsecureSSL = $Global:MoveSessions.Default.AllowInsecureSSL ?? $existingSession.AllowInsecureSSL
                    }
                    New-MoveSession @newSessionSplat -Force
                } catch {
                    $Global:MoveSessions.Clear()
                    Write-Verbose 'Error creating refreshed session'
                    throw $_
                }

            }
        } catch {
            Write-Verbose "No valid MoveSession found at path: $sessionPath"
            Write-Verbose "Reason: $_"
            $existingSession = $null
        }
    }

    process {
        if ($existingSession) {
            Write-Verbose "Returning existing MoveSession: $($existingSession.Name)@$($existingSession.MoveServer)"
            return $existingSession
        }

        [string] $StarMatch = '.*\*.*'

        $SessionsToReturn = switch ($PSCmdlet.ParameterSetName) {
            'ByObject' {
                $Global:MoveSessions.Values | Where-Object {
                    $_.Name -eq $MoveSession.Name -and
                    $_.TMServer -eq $MoveSession.TMServer -and
                    $_.TMVersion -eq $MoveSession.TMVersion
                }
                break
            }

            'ByName' {
                if ( $Name -eq '*' ) {
                    $Global:MoveSessions.Values
                } else {
                    foreach ($SingleName in $Name) {
                        $Global:MoveSessions.Values | Where-Object Name -Like $SingleName
                    }
                }
                break
            }

            'ByServer' {
                if ($Server.Count -eq 1 -and $Server -match $StarMatch) {
                    # Adding a star at the end so we can match just by name and not by FQDN
                    $Global:MoveSessions.Values | Where-Object MoveServer -Like "$Server*"
                } else {
                    $Global:MoveSessions.Values | Where-Object MoveServer -In $Server
                }
                break
            }

            'ByVersion' {
                if ($Version.Count -eq 1 -and $Version -match $StarMatch) {
                    $Global:MoveSessions.Values | Where-Object MoveVersion -Like $Version
                } else {
                    $Global:MoveSessions.Values | Where-Object MoveVersion -In $Version
                }
                break
            }
        }

        if ( $SessionsToReturn.Count ) {
            Write-Verbose "Returning $($SessionsToReturn.Count) sessions"
            return [MoveSession] $SessionsToReturn
        }

        switch ($PSCmdlet.ParameterSetName) {
            'ByObject' {
                throw ('Unexpected error returning a MoveSession with Name: "{0}" on server "{1}"' -f ($MoveSession)?.Name, ($MoveSession).TMServer)
            }

            default {
                $MatchingProperty = $PSCmdlet.ParameterSetName -replace '^By'
                $MatchingPropertyValue = (Get-Variable $MatchingProperty -ErrorAction SilentlyContinue)?.Value
                if ( $MatchingPropertyValue -match $StarMatch) {
                    Write-Verbose 'No MoveSessions found'
                    return @{}
                } else {
                    throw "MoveSession with provided $MatchingProperty '$MatchingPropertyValue' was not found"
                }
            }
        }
    }
}

function Test-MoveSession {
    <#
    .SYNOPSIS
    Tests the connection to a MoveSession
 
    .PARAMETER MoveSession
    The MoveSession to test
 
    .PARAMETER MoveSession
    Test with Server name only
 
    .EXAMPLE
    Test-MoveSession -MoveSession (Get-MoveSession -Name 'Default')
 
    .EXAMPLE
    Test-MoveSession -Server my.nutanix.move.net
 
    .OUTPUTS
    None
    #>


    [CmdletBinding()]
    param(
        [AllowNull()][Parameter(
            Mandatory = $false,
            Position = 0,
            ValueFromPipeline = $true,
            ParameterSetName = 'MoveSessionObject')]
        [pscustomobject]$MoveSession,

        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'MoveSessionServer')]
        [string]$Server
    )

    process {
        if ($PSCmdlet.ParameterSetName -eq 'MoveSessionServer') {
            $sessionPath = Join-Path -Path $MoveSessionsPath -ChildPath $Server
            if (Test-Path -Path $sessionPath -ErrorAction SilentlyContinue) {
                $NewMoveSession = Get-Content -Path $sessionPath -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
                Test-MoveSession -MoveSession $NewMoveSession
            } else {
                return $false
            }
        }

        if ($null -eq $MoveSession) {
            return $false
        }

        $expirationDateTime = $MoveSession.ExpirationDate
        $now = Get-Date

        $isExpired = ($expirationDateTime - $now).totalseconds -lt 20
        if ($isExpired) {
            Write-Verbose "it is expired: expirationDate = $expirationdatetime; now = $now"
            return $false
        }

        if ($MoveSession.MoveWebSession) {
            Write-Verbose "Testing Move Session [$($MoveSession.Name)] on server [$($MoveSession.MoveServer)]"
            $testTokenSplat = @{
                Method               = 'POST'
                Uri                  = 'https://{0}:{1}/move/v2/providers/list' -f $MoveSession.MoveServer, $MoveSession.MovePort
                Headers              = @{
                    Authorization   = "Bearer $($MoveSession.MoveWebSession.Headers.Authorization)"
                    'Content-Type'  = 'application/json'
                    'Accept'        = 'application/json'
                    'Cache-Control' = 'no-cache'
                }
                SkipCertificateCheck = $MoveSession.AllowInsecureSSL.IsPresent
                StatusCodeVariable   = 'statusCode'
            }
            try {
                $null = Invoke-RestMethod @testTokenSplat
                if ($statusCode -eq 200) {
                    Write-Verbose "Move Session [$($MoveSession.Name)] on server [$($MoveSession.MoveServer)] is valid"
                    return $true
                } else {
                    Write-Verbose "Move Session [$($MoveSession.Name)] on server [$($MoveSession.MoveServer)] is not valid, status code: $statusCode"
                    return $false
                }
            } catch {
                Write-Verbose "Error: $_"
                return $false
            }
        } else {
            Write-Verbose "No Move Web Session found for server [$($MoveSession.TMServer)]"
            return $false
        }
    }
}