Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v2] ssh keypassphrase port from v3 #2232

Merged
merged 5 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)

## `5.26.3`

- BugFix: Fixed issue in local web help with highlighted sidebar item getting out of sync. [#2215](https://github.com/zowe/zowe-cli/pull/2215)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,41 @@ import { join } from "path";
import { ConfigAutoStore } from "../../../config/src/ConfigAutoStore";
import { setupConfigToLoad } from "../../../../__tests__/src/TestUtil";
import { IOverridePromptConnProps } from "../../src/session/doc/IOverridePromptConnProps";

const certFilePath = join(__dirname, "..", "..", "..", "..", "__tests__", "__integration__", "cmd",
"__tests__", "integration", "cli", "auth", "__resources__", "fakeCert.cert");
const certKeyFilePath = join(__dirname, "..", "..", "..", "..", "__tests__", "__integration__", "cmd",
"__tests__", "integration", "cli", "auth", "__resources__", "fakeKey.key");

import { ISshSession } from "../../../../../zosuss/lib/doc/ISshSession";
const certFilePath = join(
__dirname,
"..",
"..",
"..",
"..",
"__tests__",
"__integration__",
"cmd",
"__tests__",
"integration",
"cli",
"auth",
"__resources__",
"fakeCert.cert"
);
const certKeyFilePath = join(
__dirname,
"..",
"..",
"..",
"..",
"__tests__",
"__integration__",
"cmd",
"__tests__",
"integration",
"cli",
"auth",
"__resources__",
"fakeKey.key"
);
interface extendedSession extends ISession {
someKey?: string
someKey?: string;
}

describe("ConnectionPropsForSessCfg tests", () => {
Expand Down Expand Up @@ -1373,4 +1400,68 @@ describe("ConnectionPropsForSessCfg tests", () => {
expect(sessCfgWithConnProps.cert).toBeUndefined();
expect(sessCfgWithConnProps.certKey).toBeUndefined();
});
it("should set default values for elements of propsToPromptFor()", async () => {
jest.spyOn(ConfigAutoStore, "findActiveProfile").mockReturnValueOnce([
"fruit",
"mango",
]);
await setupConfigToLoad({
profiles: {
mango: {
type: "fruit",
properties: {},
secure: ["host"],
},
},
defaults: { fruit: "mango" },
});
const overrides: IOverridePromptConnProps[] = [
{
propertyName: "someKey",
argumentName: "someKeyOther",
propertiesOverridden: [
"password",
"tokenType",
"tokenValue",
"cert",
"certKey",
],
},
];
const passFromPrompt = "somePass";
const initialSessCfg: extendedSession = {
hostname: "SomeHost",
port: 20,
user: "FakeUser",
rejectUnauthorized: true,
};
const args = {
$0: "zowe",
_: [""],
someKey: "somekeyvalue",
};

const commandHandlerPrompt = jest.fn(() => {
return Promise.resolve(passFromPrompt);
});
const parms = {
response: {
console: {
prompt: commandHandlerPrompt,
},
},
};
const sessCfgWithConnProps: ISshSession =
await ConnectionPropsForSessCfg.addPropsOrPrompt<ISshSession>(
initialSessCfg,
args,
{
doPrompting: true,
propertyOverrides: overrides,
propsToPromptFor: [{name: "keyPassphrase",isGivenValueValid: string => true}],
parms: parms as any,
}
);
expect((ConnectionPropsForSessCfg as any).secureSessCfgProps).toContain("keyPassphrase");
});
});
Loading
Loading