diff --git a/client/src/components/profile.ts b/client/src/components/profile.ts index 9ef9766d4..bbc5a4b22 100644 --- a/client/src/components/profile.ts +++ b/client/src/components/profile.ts @@ -88,6 +88,7 @@ export interface SSHProfile extends BaseProfile { saspath: string; port: number; username: string; + identityFile: string; } export interface COMProfile extends BaseProfile { @@ -573,6 +574,14 @@ export class ProfileConfig { return; } + profileClone.identityFile = await createInputTextBox( + ProfilePromptType.IdentityFile, + profileClone.identityFile, + ); + if (profileClone.identityFile === undefined) { + return; + } + profileClone.port = parseInt( await createInputTextBox(ProfilePromptType.Port, DEFAULT_SSH_PORT), ); @@ -657,6 +666,7 @@ export enum ProfilePromptType { SASPath, Port, Username, + IdentityFile, } /** @@ -794,6 +804,13 @@ const input: ProfilePromptInput = { placeholder: l10n.t("Enter your username"), description: l10n.t("Enter your SAS server username."), }, + [ProfilePromptType.IdentityFile]: { + title: l10n.t("SAS Server Private Key"), + placeholder: l10n.t("(Optional) Enter your private key path"), + description: l10n.t( + "(Optional) Enter path to private key for your SAS server.", + ), + }, }; /** diff --git a/client/src/connection/ssh/index.ts b/client/src/connection/ssh/index.ts index 60b5cfad7..66858edcc 100644 --- a/client/src/connection/ssh/index.ts +++ b/client/src/connection/ssh/index.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { l10n } from "vscode"; +import { readFileSync } from "fs"; import { Client, ClientChannel, ConnectConfig } from "ssh2"; import { BaseConfig, RunResult } from ".."; @@ -18,12 +19,18 @@ export interface Config extends BaseConfig { username: string; saspath: string; port: number; + identityFile: string; } export function getSession(c: Config): Session { - if (!process.env.SSH_AUTH_SOCK) { + if ( + !process.env.SSH_AUTH_SOCK && + !(c.identityFile && c.identityFile.length) + ) { throw new Error( - l10n.t("SSH_AUTH_SOCK not set. Check Environment Variables."), + l10n.t( + "SSH_AUTH_SOCK not set and no identityFile provided. Check Environment Variables and profile.", + ), ); } @@ -67,12 +74,19 @@ export class SSHSession extends Session { return; } + // Exception bubbles up usefully if this is set, but the file doesn't exist + let privateKey = undefined; + if (this._config.identityFile && this._config.identityFile.length) { + privateKey = readFileSync(this._config.identityFile); + } + const cfg: ConnectConfig = { host: this._config.host, port: this._config.port, username: this._config.username, readyTimeout: sasLaunchTimeout, agent: process.env.SSH_AUTH_SOCK || undefined, + privateKey: privateKey, }; this.conn diff --git a/client/test/components/profile/profile.test.ts b/client/test/components/profile/profile.test.ts index 3ef72a670..2ace3d59c 100644 --- a/client/test/components/profile/profile.test.ts +++ b/client/test/components/profile/profile.test.ts @@ -654,6 +654,7 @@ describe("Profiles", async function () { sasOptions: ["-nonews"], saspath: "/sas/path", username: "username", + identityFile: "", }; // Arrange // Act diff --git a/client/test/connection/ssh/index.test.ts b/client/test/connection/ssh/index.test.ts index ddc217052..c3e69940b 100644 --- a/client/test/connection/ssh/index.test.ts +++ b/client/test/connection/ssh/index.test.ts @@ -25,6 +25,7 @@ describe("ssh connection", () => { saspath: "/path/to/sas_u8", sasOptions: [], agentSocket: "/agent/socket", + identityFile: "", }; session = new SSHSession(config); @@ -46,6 +47,7 @@ describe("ssh connection", () => { port: 22, saspath: "/path/to/sas_u8", sasOptions: [], + identityFile: "", }; sandbox.stub(Client.prototype, "connect").callsFake(function () { @@ -85,6 +87,7 @@ describe("ssh connection", () => { saspath: "/path/to/sas_u8", sasOptions: [], agentSocket: "/agent/socket", + identityFile: "", }; sandbox.stub(Client.prototype, "connect").callsFake(function () { @@ -106,6 +109,7 @@ describe("ssh connection", () => { saspath: "/path/to/sas_u8", sasOptions: [], agentSocket: "/agent/socket", + identityFile: "", }; sandbox.stub(Client.prototype, "connect").callsFake(function () { @@ -133,6 +137,7 @@ describe("ssh connection", () => { saspath: "/path/to/sas_u8", sasOptions: [], agentSocket: "/agent/socket", + identityFile: "", }; sandbox.stub(Client.prototype, "connect").callsFake(function () { @@ -159,6 +164,7 @@ describe("ssh connection", () => { saspath: "/path/to/sas_u8", sasOptions: [], agentSocket: "/agent/socket", + identityFile: "", }; const session = new SSHSession(config); @@ -339,6 +345,7 @@ describe("ssh connection", () => { saspath: "saspath", sasOptions: ["-nonews"], port: 22, + identityFile: "", }; }); diff --git a/package.json b/package.json index d1ec7ac03..f43c5d0e3 100644 --- a/package.json +++ b/package.json @@ -297,7 +297,8 @@ "host", "saspath", "username", - "port" + "port", + "identityFile" ], "properties": { "host": { @@ -321,6 +322,11 @@ "description": "%configuration.SAS.connectionProfiles.profiles.ssh.port%", "exclusiveMinimum": 1, "exclusiveMaximum": 65535 + }, + "identityFile": { + "type": "string", + "default": "", + "description": "%configuration.SAS.connectionProfiles.profiles.ssh.identityFile%" } } } diff --git a/package.nls.json b/package.nls.json index a404c1d90..18e516889 100644 --- a/package.nls.json +++ b/package.nls.json @@ -50,6 +50,7 @@ "configuration.SAS.connectionProfiles.profiles.ssh.port": "SAS SSH Connection port", "configuration.SAS.connectionProfiles.profiles.ssh.saspath": "SAS SSH Connection executable path", "configuration.SAS.connectionProfiles.profiles.ssh.username": "SAS SSH Connection username", + "configuration.SAS.connectionProfiles.profiles.ssh.identityFile": "SAS SSH Connection Identity File", "configuration.SAS.flowConversionMode": "Choose the conversion mode for notebooks", "configuration.SAS.flowConversionModeNode": "Convert each notebook cell to a node", "configuration.SAS.flowConversionModeSwimlane": "Convert each notebook cell to a swimlane", diff --git a/website/docs/Configurations/Profiles/sas9ssh.md b/website/docs/Configurations/Profiles/sas9ssh.md index 3cabe15a9..c9d3a52fb 100644 --- a/website/docs/Configurations/Profiles/sas9ssh.md +++ b/website/docs/Configurations/Profiles/sas9ssh.md @@ -4,7 +4,10 @@ sidebar_position: 4 # SAS 9.4 (remote - SSH) Connection Profile -For a secure connection to SAS 9.4 (remote - SSH) server, a public / private SSH key pair is required. The socket defined in the environment variable `SSH_AUTH_SOCK` is used to communicate with ssh-agent to authenticate the SSH session. The private key must be registered with the ssh-agent. The steps for configuring SSH follow. +For a secure connection to SAS 9.4 (remote - SSH) server, a public / private SSH key pair is required. There are two ways to provide a Private Key: + +1. (Preferred) The socket defined in the environment variable `SSH_AUTH_SOCK` is used to communicate with ssh-agent to authenticate the SSH session. The private key must be registered with the ssh-agent. The steps for configuring SSH follow. +1. (Non-Preferred) When creating an SSH profile, you will be asked for an identityFile. This is the full path to a private key. Note that this private key must be created without a passphrase protecting it (hence, this is the non-preferred path). Leave this blank if using ssh-agent ## Profile Anatomy @@ -12,12 +15,13 @@ A SAS 9.4 (remote – SSH) connection profile includes the following parameters: `"connectionType": "ssh"` -| Name | Description | Additional Notes | -| ---------- | ------------------------------------ | -------------------------------------------------------------------------- | -| `host` | SSH Server Host | Appears when hovering over the status bar. | -| `username` | SSH Server Username | The username to establish the SSH connection to the server. | -| `port` | SSH Server Port | The SSH port of the SSH server. The default value is 22. | -| `saspath` | Path to SAS Executable on the server | Must be a fully qualified path on the SSH server to a SAS executable file. | +| Name | Description | Additional Notes | +| -------------- | ------------------------------------ | -------------------------------------------------------------------------- | +| `host` | SSH Server Host | Appears when hovering over the status bar. | +| `username` | SSH Server Username | The username to establish the SSH connection to the server. | +| `port` | SSH Server Port | The SSH port of the SSH server. The default value is 22. | +| `saspath` | Path to SAS Executable on the server | Must be a fully qualified path on the SSH server to a SAS executable file. | +| `identityFile` | Full path to private key | Must be a fully qualified path on local host to an unshielded private key. | ## Required setup for connection to SAS 9.4 (remote - SSH) @@ -72,7 +76,7 @@ Note: if ~/.ssh/config does not exist, run the following Powershell command to c Note: the default path to the SAS executable (saspath) is /opt/sasinside/SASHome/SASFoundation/9.4/bin/sas_u8. Check with your SAS administrator for the exact path. -9. Add the public part of the keypair to the SAS server. Add the contents of the key file to the ~/.ssh/authorized_keys file. +9. Add the public part of the keypair to the SAS server. Add the contents of the key file to the ~/.ssh/authorized_keys file. If you have it on your system, you can use "ssh-copy-id" command to make this easy. By default, `ssh-copy-id host.machine.name` will probably do what you need. ### Mac @@ -111,4 +115,21 @@ Host host.machine.name } ``` -6. Add the public part of the keypair to the SAS server. Add the contents of the key file to the ~/.ssh/authorized_keys file. +6. Add the public part of the keypair to the SAS server. Add the contents of the key file to the ~/.ssh/authorized_keys file. If you have it on your system, you can use "ssh-copy-id" command to make this easy. By default, `ssh-copy-id host.machine.name` will probably do what you need. + +## Windows/Mac/Linux using only identityFile + +1. Create a keypair and copy to the server, as described in sections above + 1. It must be created without a passphrase +1. In the profile, provide the full path to your private key: + +```json +"ssh_test": { + "connectionType": "ssh", + "host": "host.machine.name", + "saspath": "/path/to/sas/executable", + "username": "username", + "port": 22, + "identityFile": "c:\\Users\\username\\.ssh\\id_ed25519" +} +```