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 Get-MoveSessionFilePath { <# .SYNOPSIS Builds the on-disk session file path for a given Move server. .DESCRIPTION Each Move server gets its own session file so that connections to multiple servers can be persisted side-by-side. The server name is normalized (the protocol prefix is stripped and any characters that are illegal in a file name are replaced) and used as the file name. .PARAMETER Server The Move server hostname or URI to build the session file path for. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Server ) # Normalize: drop the protocol prefix, then sanitize illegal file name chars $instance = $Server -replace '^https?://' $invalid = [regex]::Escape(-join [IO.Path]::GetInvalidFileNameChars()) $safeName = $instance -replace "[$invalid]", '_' return Join-Path -Path $MoveSessionsPath -ChildPath ('{0}.json' -f $safeName) } 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 { # Each session is named after (and persisted per) its server, so that # connections to multiple servers can coexist. $SessionName = $Server -replace '^https?://' # 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 = Get-MoveSessionFilePath -Server $SessionName $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 Token created with auth header (truncated): $($global:MoveSessions[$SessionName].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 { # The in-memory and on-disk caches are keyed per-server, so the fast-path # (return a cached/persisted session) only applies when we can pin down a # single, exact server. Wildcard, Name and Version lookups fall through to # the process block, which filters the in-memory sessions. $targetServer = $null switch ($PSCmdlet.ParameterSetName) { 'ByServer' { if ($Server -and $Server -notmatch '\*') { $targetServer = $Server } } 'ByObject' { $targetServer = $MoveSession.MoveServer } } if (-not $targetServer) { Write-Verbose ' GET: No single server target; deferring to in-memory lookup' return # go to the process block } $sessionKey = $targetServer -replace '^https?://' $sessionPath = Get-MoveSessionFilePath -Server $sessionKey # Is there a valid session for this server in the Global:MoveSessions variable? if (Test-MoveSession -MoveSession $Global:MoveSessions[$sessionKey]) { Write-Verbose "GET: Returning valid session for [$sessionKey] from Global var" $existingSession = $Global:MoveSessions[$sessionKey] return # go to the process block } else { Write-Verbose "GET: No valid session for [$sessionKey] in Global var" $Global:MoveSessions.Remove($sessionKey) } # Is there a valid session saved to disk for this server? Write-Verbose (' GET: Checking for a session on disk: Server = {0}' -f $sessionKey) try { $existingSessionJson = Get-Content -Path $sessionPath -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop $existingSession = [MoveSession]::new( $sessionKey, $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[$sessionKey] = $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[$sessionKey].MoveServer ?? $existingSession.MoveServer Port = $Global:MoveSessions[$sessionKey].MovePort ?? $existingSession.MovePort Credential = $Credential AllowInsecureSSL = $Global:MoveSessions[$sessionKey].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 $sessionKey, $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( $sessionKey, $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[$sessionKey] = $retrySession $existingSession = $retrySession return } } $existingSession = New-MoveSession @newSessionSplat -Force -Passthru } finally { Write-Verbose "LOCK: Releasing [$lockFileName]" $lock.Dispose() } } catch { $Global:MoveSessions.Remove($sessionKey) 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 [$($existingSession.Name)] - exp $($existingSession.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 = Get-MoveSessionFilePath -Server $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 } } } |