Public/Enter-OnePAMSSH.ps1
|
function Enter-OnePAMSSH { <# .SYNOPSIS Starts an interactive SSH session to a OnePAM resource. .DESCRIPTION Resolves the resource, creates a gateway session, and opens a WebSocket-based terminal relay. The session is recorded and audited by OnePAM. .PARAMETER Resource Resource name or UUID. Accepts user@resource syntax. .PARAMETER User SSH login user (overrides user@ prefix). .PARAMETER Command Remote command to execute instead of opening a shell. .EXAMPLE Enter-OnePAMSSH -Resource "my-server" .EXAMPLE Enter-OnePAMSSH -Resource "root@my-server" .EXAMPLE Enter-OnePAMSSH -Resource "my-server" -Command "uname -a" #> [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [string]$Resource, [Alias('l')] [string]$User, [string]$Command ) $login = $User $resourceName = $Resource if ($Resource -match '^(.+)@(.+)$' -and -not $User) { $login = $Matches[1] $resourceName = $Matches[2] } $res = Get-OnePAMResource -Name $resourceName if (-not $res) { throw "Resource '$resourceName' not found." } $resId = if ($res.id) { $res.id } elseif ($res.ID) { $res.ID } else { throw "Cannot determine resource ID." } Assert-OpSafePathSegment -Value $resId -Label 'resource ID' if ($res.type -and $res.type -ne 'ssh') { throw "Resource '$resourceName' is type '$($res.type)', not SSH." } $connectBody = @{} if ($login) { $connectBody['ssh_user'] = $login } $session = Invoke-OpApi -Method POST -Path "/api/v1/resources/$resId/connect" -Body $connectBody if (-not $session.session_id) { throw 'Failed to create session: no session_id returned.' } Assert-OpSafePathSegment -Value $session.session_id -Label 'session ID' $cfg = Get-OpConfig if ($session.direct) { $baseUri = [Uri]$cfg.api_base $wsScheme = if ($baseUri.Scheme -eq 'https') { 'wss' } else { 'ws' } $hostPort = $baseUri.Authority } else { if (-not $session.proxy_host) { throw 'Session response missing proxy_host.' } $wsScheme = 'wss' $hostPort = $session.proxy_host if ($session.proxy_port -and $session.proxy_port -ne 443) { $hostPort = "$($session.proxy_host):$($session.proxy_port)" } } $connectPath = if ($session.connect_url) { $session.connect_url } else { "/gateway/ssh/$($session.session_id)" } $queryParts = [System.Collections.Generic.List[string]]::new() if (-not $session.direct -and $session.token) { $queryParts.Add("token=$([System.Uri]::EscapeDataString($session.token))") } if ($Command) { $queryParts.Add("mode=exec") $queryParts.Add("command=$([System.Uri]::EscapeDataString($Command))") } $wsUrl = "${wsScheme}://${hostPort}${connectPath}" if ($queryParts.Count -gt 0) { $wsUrl += "?$($queryParts -join '&')" } $ws = [System.Net.WebSockets.ClientWebSocket]::new() $ws.Options.SetRequestHeader('User-Agent', "onepam-powershell/$script:ModuleVersion") if ($session.direct) { $token = Get-OpToken if ($token) { $ws.Options.SetRequestHeader('Authorization', "Bearer $($token.access_token)") } } $connectCts = [System.Threading.CancellationTokenSource]::new([TimeSpan]::FromSeconds(15)) try { $ws.ConnectAsync([Uri]$wsUrl, $connectCts.Token).GetAwaiter().GetResult() } catch { $ws.Dispose() throw "WebSocket connection failed: $_" } finally { $connectCts.Dispose() } Write-Host "Connected to $resourceName (session: $($session.session_id))" -ForegroundColor Green if ($Command) { Write-Host "Executing: $Command" -ForegroundColor Gray } Write-Host '' try { $cols = [Console]::WindowWidth $rows = [Console]::WindowHeight $resizeMsg = @{ type = 'resize'; cols = $cols; rows = $rows } | ConvertTo-Json -Compress Send-OpWsMessage -WebSocket $ws -Message $resizeMsg } catch { } $stdinBuffer = [byte[]]::new(4096) $stdin = [Console]::OpenStandardInput() $stdout = [Console]::OpenStandardOutput() $cts = [System.Threading.CancellationTokenSource]::new() $receiveTask = [System.Threading.Tasks.Task]::Run({ $buf = [byte[]]::new(65536) $seg = [ArraySegment[byte]]::new($buf) try { while ($ws.State -eq [System.Net.WebSockets.WebSocketState]::Open -and -not $cts.Token.IsCancellationRequested) { $ms = [System.IO.MemoryStream]::new() try { do { $result = $ws.ReceiveAsync($seg, $cts.Token).GetAwaiter().GetResult() if ($result.MessageType -eq [System.Net.WebSockets.WebSocketMessageType]::Close) { $cts.Cancel() return } $ms.Write($buf, 0, $result.Count) } while (-not $result.EndOfMessage) if ($result.MessageType -eq [System.Net.WebSockets.WebSocketMessageType]::Binary) { $data = $ms.ToArray() $stdout.Write($data, 0, $data.Length) $stdout.Flush() } } finally { $ms.Dispose() } } } catch [System.OperationCanceledException] { } catch { } }.GetNewClosure()) try { if ([Environment]::OSVersion.Platform -ne 'Win32NT') { $sttyState = & stty -g 2>$null & stty raw -echo 2>$null } while ($ws.State -eq [System.Net.WebSockets.WebSocketState]::Open -and -not $cts.Token.IsCancellationRequested) { try { $bytesRead = $stdin.Read($stdinBuffer, 0, $stdinBuffer.Length) if ($bytesRead -le 0) { $cts.Cancel() break } $segment = [ArraySegment[byte]]::new($stdinBuffer, 0, $bytesRead) $ws.SendAsync($segment, [System.Net.WebSockets.WebSocketMessageType]::Binary, $true, $cts.Token).GetAwaiter().GetResult() } catch [System.OperationCanceledException] { break } catch { break } } } finally { if ([Environment]::OSVersion.Platform -ne 'Win32NT' -and $sttyState) { & stty $sttyState 2>$null } $cts.Cancel() try { $receiveTask.Wait(2000) } catch { } Close-OpWebSocket -WebSocket $ws $cts.Dispose() $stdin.Dispose() $stdout.Dispose() Write-Host '' Write-Host "Session ended." -ForegroundColor Gray } } |