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 Lock-FileMutex {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Path,
        [int]$TimeoutSeconds = 15
    )

    begin {
        Write-Verbose "LOCK: Attempting [$Path]"
        $stopWatch = [Diagnostics.Stopwatch]::StartNew()
    }

    process {
        while ($stopWatch.Elapsed -lt [TimeSpan]::FromSeconds($TimeoutSeconds)) {
            try {
                $fs = [IO.File]::Open(
                    $Path, 'OpenOrCreate', 'ReadWrite', 'None'
                )

                Write-Verbose "LOCK: Acquired [$Path]"
                return $fs
            }
            catch [IO.IOException] {
                Write-Verbose "LOCK: Busy, retrying..."
                Start-Sleep -Milliseconds 88
            }
        }

        throw "LOCK: Timeout acquiring [$Path]"
    }
}


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 -Credential $Credential
            } catch {
                Write-Verbose "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 ' ]'
        }

        try {
            $vp = $VerbosePreference; $VerbosePreference = 'SilentlyContinue'
            $Response = Invoke-WebRequest @WebRequestSplat
            $loginRequestTime = Get-Date
            $VerbosePreference = $vp
            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 ' NEW: Populating NewMoveSession'
        $MoveWebSession.Headers['Authorization'] = $ResponseContent.Status.Token
        Write-Verbose " NEW: new TOK = $($MoveWebSession.Headers['Authorization']?.Substring(270))"
        $NewMoveSession.setExpirationDate($loginRequestTime, $ResponseContent.Status)
        $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 ' NEW: Exported NewMoveSession, adding to Global:MoveSessions'
        $Global:MoveSessions[$SessionName] = $NewMoveSession
        Write-Verbose " NEW: TOK1 = $($global:movesessions.Default.MoveWebSession.Headers.Authorization?.Substring(270))"

        ## 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,
            ValueFromPipelineByPropertyName = $true)]
        [pscredential]$Credential,

        [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 'GET: Returning valid session from Global var'
            $existingSession = $Global:MoveSessions.Default
            return # go to the process block
        } else {
            Write-Verbose 'GET: No valid sessions in Global var'
            $Global:MoveSessions.Clear()
        }

        # Is there a valid session saved to disk?
        $message = ' GET: Checking for a session in disk: '
        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()
            foreach ($header in $existingSessionJson.MoveWebSession.Headers.psobject.Members | Where-Object membertype -EQ NoteProperty) {
                $existingWebSession.Headers.Add($header.Name, $header.Value)
            }
            $existingSession.ExpirationDate = $existingSessionJson.ExpirationDate
            $existingSession.MoveWebSession = $existingWebSession
            $existingSession.MoveWebSession.Cookies = [System.Net.CookieContainer]::new()

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

                try {
                    Write-Verbose ' GET: Creating New Session -Force'
                    # 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       = $Credential
                        AllowInsecureSSL = $Global:MoveSessions.Default.AllowInsecureSSL ?? $existingSession.AllowInsecureSSL
                    }

                    if (-not ($newSessionSplat.Credential -is [pscredential])) {
                        try {
                            $newSessionSplat.Credential = Get-StoredCredential -Name $newSessionSplat.Server
                        }
                        catch {
                            throw "No stored credential: {0}" -f $newSessionSplat.Server
                        }
                    }
                    
                    try {
                        $lockFileName = '{0}_{1}.lock' -f $newSessionSplat.Server, $newSessionSplat.Port
                        $lockPath = Join-Path -Path $MoveSessionsPath -ChildPath $lockFileName
                        $lock     = Lock-FileMutex $lockPath

                        if (Test-Path $sessionPath) {
                            $retry = Get-Content $sessionPath -Raw | ConvertFrom-Json

                            $retrySession = [MoveSession]::new(
                                'Default',
                                $retry.MoveServer,
                                $retry.MovePort,
                                $retry.AllowInsecureSSL
                            )

                            $retryWebSession = [Microsoft.PowerShell.Commands.WebRequestSession]::new()
                            foreach ($header in $retry.MoveWebSession.Headers.psobject.Members | Where-Object membertype -EQ NoteProperty) {
                                $retryWebSession.Headers.Add($header.Name, $header.Value)
                            }
                            $retrySession.ExpirationDate = $retry.ExpirationDate
                            $retrySession.MoveWebSession = $retryWebSession
                            $retrySession.MoveWebSession.Cookies = [System.Net.CookieContainer]::new()

                            if (Test-MoveSession -MoveSession $retrySession) {
                                $Global:MoveSessions.Default = $retrySession
                                $existingSession = $retrySession
                                return
                            }
                        }

                        $existingSession = New-MoveSession @newSessionSplat -Force -Passthru
                    }
                    finally {
                        Write-Verbose "LOCK: Releasing [$lockFileName]"
                        $lock.Dispose()
                    }

                } catch {
                    $Global:MoveSessions.Clear()
                    Write-Verbose ' GET: Error creating refreshed session'
                    throw $_
                }

            }
        } catch {
            Write-Verbose " GET: No valid MoveSession in [$sessionPath]: $_"
            $existingSession = $null
        }
    }

    process {
        if ($existingSession) {
            Write-Verbose " GET: Returning existing MoveSession. TOK [$($global:MoveSessions.Default.MoveWebSession.Headers.Authorization.Substring(270))] - exp $($global:MoveSessions.Default.ExpirationDate)"
            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 " GET: 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 ' GET: 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') {
            Write-Verbose ' TST: PSN=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 {
                Write-Verbose ' TST: FALSE bc no sessionPath'
                return $false
            }
        }

        if ($null -eq $MoveSession) {
            Write-Verbose " TST: FALSE bc null = MoveSession"
            return $false
        }

        if ($MoveSession.MoveWebSession) {
            Write-Verbose " TST: Testing Move Session [$($MoveSession.Name)] on server [$($MoveSession.MoveServer)]"
            $expirationDateTime = $MoveSession.ExpirationDate
            $isExpired = (Get-Date) -ge $expirationDateTime
            if ($isExpired) {
                Write-Verbose " TST: it is expired: expirationDate = $expirationdatetime; now = $now"
                return $false
            }
            $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 {
                $vp = $VerbosePreference; $VerbosePreference = 'SilentlyContinue'
                $null = Invoke-RestMethod @testTokenSplat
                $VerbosePreference = $vp
                if ($statusCode -eq 200) {
                    Write-Verbose " TST: Move Session [$($MoveSession.Name)] on server [$($MoveSession.MoveServer)] is valid"
                    return $true
                } else {
                    Write-Verbose " TST: Move Session [$($MoveSession.Name)] on server [$($MoveSession.MoveServer)] is not valid, status code: $statusCode"
                    return $false
                }
            } catch {
                Write-Verbose " TST: Error: $_"
                return $false
            }
        } else {
            Write-Verbose " TST: No Move Web Session found for server [$($MoveSession.TMServer)]"
            return $false
        }
    }
}