Skip to content

Commit

Permalink
Merge pull request #2214 from zowe/ssh-keypassphrase-prompt
Browse files Browse the repository at this point in the history
[v3] Ssh keypassphrase prompt
  • Loading branch information
t1m0thyj committed Aug 9, 2024
2 parents b600db9 + ff16d35 commit d64090a
Show file tree
Hide file tree
Showing 12 changed files with 1,234 additions and 422 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@

jest.mock("../../../../../../zosuss/lib/Shell");

import { IHandlerParameters, IProfile, CommandProfiles } from "@zowe/imperative";
import * as SshHandler from "../../../../../src/zosuss/issue/ssh/Ssh.handler";
import { IHandlerParameters, IProfile, CommandProfiles, ConnectionPropsForSessCfg } from "@zowe/imperative";
import SshHandler from "../../../../../src/zosuss/issue/ssh/Ssh.handler";
import * as SshDefinition from "../../../../../src/zosuss/issue/ssh/Ssh.definition";
import { Shell } from "@zowe/zos-uss-for-zowe-sdk";
import { mockHandlerParameters } from "@zowe/cli-test-utils";
Expand All @@ -33,6 +33,20 @@ const UNIT_TEST_SSH_PROF_OPTS_PRIVATE_KEY = {
user: "someone",
privateKey: normalize(join(__dirname, "..", "..", "..", "..", "..", "..", "zosuss", "__tests__", "__unit__", "__resources__", "fake_id_rsa"))
};
const UNIT_TEST_SSH_PROF_OPTS_PRIVATE_KEY_WITH_PASSPHRASE = {
host: "somewhere.com",
port: "22",
user: "someone",
privateKey: normalize(join(__dirname, "..", "..", "..", "..", "..", "..", "zosuss", "__tests__", "__unit__", "__resources__", "fake_id_rsa")),
keyPassPhrase: "dummyPassPhrase123"
};
const UNIT_TEST_SSH_PROF_OPTS_PRIVATE_KEY_WITH_PASSPHRASE_NO_USER = {
host: "somewhere.com",
port: "22",
privateKey: normalize(join(__dirname, "..", "..", "..", "..", "..", "..", "zosuss", "__tests__", "__unit__", "__resources__", "fake_id_rsa")),
keyPassPhrase: "dummyPassPhrase123"
};


// A mocked profile map with ssh profile
const UNIT_TEST_PROFILE_MAP = new Map<string, IProfile[]>();
Expand All @@ -53,7 +67,26 @@ UNIT_TEST_PROFILE_MAP_PRIVATE_KEY.set(
...UNIT_TEST_SSH_PROF_OPTS_PRIVATE_KEY
}]
);
const UNIT_TEST_PROFILE_MAP_PRIVATE_KEY_WITH_PASSPHRASE = new Map<string, IProfile[]>();
UNIT_TEST_PROFILE_MAP_PRIVATE_KEY_WITH_PASSPHRASE.set(
"ssh", [{
name: "ssh",
type: "ssh",
...UNIT_TEST_SSH_PROF_OPTS_PRIVATE_KEY_WITH_PASSPHRASE
}]
);
const UNIT_TEST_PROFILE_MAP_PRIVATE_KEY_WITH_PASSPHRASE_NO_USER = new Map<string, IProfile[]>();
UNIT_TEST_PROFILE_MAP_PRIVATE_KEY_WITH_PASSPHRASE.set(
"ssh", [{
name: "ssh",
type: "ssh",
...UNIT_TEST_SSH_PROF_OPTS_PRIVATE_KEY_WITH_PASSPHRASE_NO_USER
}]
);

const UNIT_TEST_PROFILES_SSH_PRIVATE_KEY = new CommandProfiles(UNIT_TEST_PROFILE_MAP_PRIVATE_KEY);
const UNIT_TEST_PROFILES_SSH_PRIVATE_KEY_WITH_PASSPHRASE = new CommandProfiles(UNIT_TEST_PROFILE_MAP_PRIVATE_KEY_WITH_PASSPHRASE);
const UNIT_TEST_PROFILES_SSH_PRIVATE_KEY_WITH_PASSPHRASE_NO_USER = new CommandProfiles(UNIT_TEST_PROFILE_MAP_PRIVATE_KEY_WITH_PASSPHRASE_NO_USER);

// Mocked parameters for the unit tests
const DEFAULT_PARAMETERS: IHandlerParameters = mockHandlerParameters({
Expand All @@ -70,6 +103,19 @@ const DEFAULT_PARAMETERS_PRIVATE_KEY: IHandlerParameters = mockHandlerParameters
profiles: UNIT_TEST_PROFILES_SSH_PRIVATE_KEY
});

const DEFAULT_PARAMETERS_KEY_PASSPHRASE: IHandlerParameters = mockHandlerParameters({
arguments: UNIT_TEST_SSH_PROF_OPTS_PRIVATE_KEY_WITH_PASSPHRASE,
positionals: ["zos-uss", "issue", "ssh"],
definition: SshDefinition.SshDefinition,
profiles: UNIT_TEST_PROFILES_SSH_PRIVATE_KEY_WITH_PASSPHRASE,
});
const DEFAULT_PARAMETERS_KEY_PASSPHRASE_NO_USER: IHandlerParameters = mockHandlerParameters({
arguments: UNIT_TEST_SSH_PROF_OPTS_PRIVATE_KEY_WITH_PASSPHRASE_NO_USER,
positionals: ["zos-uss", "issue", "ssh"],
definition: SshDefinition.SshDefinition,
profiles: UNIT_TEST_PROFILES_SSH_PRIVATE_KEY_WITH_PASSPHRASE_NO_USER,
});

const testOutput = "TEST OUTPUT";

describe("issue ssh handler tests", () => {
Expand All @@ -82,37 +128,107 @@ describe("issue ssh handler tests", () => {
Shell.executeSsh = jest.fn(async (session, command, stdoutHandler) => {
stdoutHandler(testOutput);
});
const handler = new SshHandler.default();
const handler = new SshHandler();
const params = Object.assign({}, ...[DEFAULT_PARAMETERS]);
params.arguments.command = "pwd";
await handler.process(params);
expect(Shell.executeSsh).toHaveBeenCalledTimes(1);
expect(testOutput).toMatchSnapshot();
});

it("should be able to get stdout with private key and key passphrase", async () => {
Shell.executeSsh = jest.fn(async (session, command, stdoutHandler) => {
stdoutHandler(testOutput);
});
const handler = new SshHandler();
const params = Object.assign({}, ...[DEFAULT_PARAMETERS_KEY_PASSPHRASE]);
params.arguments.command = "echo test";
await handler.process(params);
expect(Shell.executeSsh).toHaveBeenCalledTimes(1);
expect(testOutput).toMatchSnapshot();
});
it("should prompt user for keyPassphrase if none is stored and privateKey requires one", async () => {
Shell.executeSsh = jest.fn(async (session, command, stdoutHandler) => {
stdoutHandler(testOutput);
});
const handler = new SshHandler();
const params = Object.assign({}, ...[DEFAULT_PARAMETERS_KEY_PASSPHRASE]);
params.arguments.command = "echo test";
jest.spyOn(handler,"processCmd").mockImplementationOnce(() => {throw new Error("but no passphrase given");});
jest.spyOn(ConnectionPropsForSessCfg as any,"getValuesBack").mockReturnValue(() => ({
keyPassphrase: "validPassword"
}));
await handler.process(params);
expect(Shell.executeSsh).toHaveBeenCalledTimes(1);
expect(testOutput).toMatchSnapshot();
});
it("should reprompt user for keyPassphrase up to 3 times if stored passphrase failed", async () => {
Shell.executeSsh = jest.fn(async (session, command, stdoutHandler) => {
stdoutHandler(testOutput);
});
const handler = new SshHandler();
const params = Object.assign({}, ...[DEFAULT_PARAMETERS_KEY_PASSPHRASE]);
params.arguments.command = "echo test";
jest.spyOn(handler,"processCmd").mockImplementationOnce(() => {throw new Error("bad passphrase?");});
jest.spyOn(ConnectionPropsForSessCfg as any,"getValuesBack").mockReturnValue(() => ({
keyPassphrase: "validPassword"
}));
await handler.process(params);
expect(Shell.executeSsh).toHaveBeenCalledTimes(1);
expect(testOutput).toMatchSnapshot();
});
it("should fail if user fails to enter incorrect key passphrase in 3 attempts", async () => {
const testOutput = "Maximum retry attempts reached. Authentication failed.";
Shell.executeSsh = jest.fn(async (session, command, stdoutHandler) => {
stdoutHandler(testOutput);
});
const handler = new SshHandler();
const params = { ...DEFAULT_PARAMETERS_KEY_PASSPHRASE };
params.arguments.command = "echo test";
jest.spyOn(handler, "processCmd").mockImplementation(() => {
throw new Error("bad passphrase?");
});
await expect(handler.process(params)).rejects.toThrow("Maximum retry attempts reached. Authentication failed.");
expect(handler.processCmd).toHaveBeenCalledTimes(4);
expect(testOutput).toMatchSnapshot();
});
it("should prompt for user and keyPassphrase if neither is stored", async () => {
const testOutput = "test";
Shell.executeSsh = jest.fn(async (session, command, stdoutHandler) => {
stdoutHandler(testOutput);
});
const handler = new SshHandler();
const params = { ...DEFAULT_PARAMETERS_KEY_PASSPHRASE_NO_USER };
params.arguments.command = "echo test";
jest.spyOn(ConnectionPropsForSessCfg as any,"getValuesBack").mockReturnValue(() => ({
user: "someone",
keyPassphrase: "validPassword"
}));
await handler.process(params);
expect(Shell.executeSsh).toHaveBeenCalledTimes(1);
expect(testOutput).toMatchSnapshot();
});
it("should be able to get stdout with privateKey", async () => {
Shell.executeSsh = jest.fn(async (session, command, stdoutHandler) => {
stdoutHandler(testOutput);
});
const handler = new SshHandler.default();
const handler = new SshHandler();
const params = Object.assign({}, ...[DEFAULT_PARAMETERS_PRIVATE_KEY]);
params.arguments.command = "pwd";
await handler.process(params);
expect(Shell.executeSsh).toHaveBeenCalledTimes(1);
expect(testOutput).toMatchSnapshot();
});

it("should be able to get stdout with cwd option", async () => {
Shell.executeSshCwd = jest.fn(async (session, command, cwd, stdoutHandler) => {
stdoutHandler(testOutput);
});
const handler = new SshHandler.default();
const handler = new SshHandler();
const params = Object.assign({}, ...[DEFAULT_PARAMETERS]);
params.arguments.command = "pwd";
params.arguments.cwd = "/user/home";
await handler.process(params);
expect(Shell.executeSshCwd).toHaveBeenCalledTimes(1);
expect(testOutput).toMatchSnapshot();
});

});
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,14 @@ exports[`issue ssh handler tests should be able to get stdout 2`] = `"TEST OUTPU

exports[`issue ssh handler tests should be able to get stdout with cwd option 1`] = `"TEST OUTPUT"`;

exports[`issue ssh handler tests should be able to get stdout with private key and key passphrase 1`] = `"TEST OUTPUT"`;

exports[`issue ssh handler tests should be able to get stdout with privateKey 1`] = `"TEST OUTPUT"`;

exports[`issue ssh handler tests should fail if user fails to enter incorrect key passphrase in 3 attempts 1`] = `"Maximum retry attempts reached. Authentication failed."`;

exports[`issue ssh handler tests should prompt for user and keyPassphrase if neither is stored 1`] = `"test"`;

exports[`issue ssh handler tests should prompt user for keyPassphrase if none is stored and privateKey requires one 1`] = `"TEST OUTPUT"`;

exports[`issue ssh handler tests should reprompt user for keyPassphrase up to 3 times if stored passphrase failed 1`] = `"TEST OUTPUT"`;
4 changes: 4 additions & 0 deletions packages/imperative/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

All notable changes to the Imperative package will be documented in this file.

## Recent Changes

- BugFix: Resolved bug that resulted in user not being prompted for a key passphrase if it is located in the secure credential array of the ssh profile. [#1770](https://github.com/zowe/zowe-cli/issues/1770)

## `8.0.0-next.202407262216`

- Update: See `5.26.1` for details
Expand Down
Loading

0 comments on commit d64090a

Please sign in to comment.