diff --git a/packages/cli/__tests__/zosuss/__unit__/issue/ssh/Ssh.handler.unit.test.ts b/packages/cli/__tests__/zosuss/__unit__/issue/ssh/Ssh.handler.unit.test.ts index 6fe5fc3b96..58c1fedba0 100644 --- a/packages/cli/__tests__/zosuss/__unit__/issue/ssh/Ssh.handler.unit.test.ts +++ b/packages/cli/__tests__/zosuss/__unit__/issue/ssh/Ssh.handler.unit.test.ts @@ -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"; @@ -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(); @@ -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(); +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(); +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({ @@ -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", () => { @@ -82,7 +128,7 @@ 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); @@ -90,23 +136,94 @@ describe("issue ssh handler tests", () => { 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"; @@ -114,5 +231,4 @@ describe("issue ssh handler tests", () => { expect(Shell.executeSshCwd).toHaveBeenCalledTimes(1); expect(testOutput).toMatchSnapshot(); }); - }); diff --git a/packages/cli/__tests__/zosuss/__unit__/issue/ssh/__snapshots__/Ssh.handler.unit.test.ts.snap b/packages/cli/__tests__/zosuss/__unit__/issue/ssh/__snapshots__/Ssh.handler.unit.test.ts.snap index 802e164178..e9f13da78c 100644 --- a/packages/cli/__tests__/zosuss/__unit__/issue/ssh/__snapshots__/Ssh.handler.unit.test.ts.snap +++ b/packages/cli/__tests__/zosuss/__unit__/issue/ssh/__snapshots__/Ssh.handler.unit.test.ts.snap @@ -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"`; diff --git a/packages/imperative/CHANGELOG.md b/packages/imperative/CHANGELOG.md index 835f8bd7e4..e5709c514e 100644 --- a/packages/imperative/CHANGELOG.md +++ b/packages/imperative/CHANGELOG.md @@ -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 diff --git a/packages/imperative/src/rest/__tests__/session/ConnectionPropsForSessCfg.unit.test.ts b/packages/imperative/src/rest/__tests__/session/ConnectionPropsForSessCfg.unit.test.ts index c0a045a32a..8ab50f1537 100644 --- a/packages/imperative/src/rest/__tests__/session/ConnectionPropsForSessCfg.unit.test.ts +++ b/packages/imperative/src/rest/__tests__/session/ConnectionPropsForSessCfg.unit.test.ts @@ -24,24 +24,50 @@ import { IOverridePromptConnProps } from "../../src/session/doc/IOverridePromptC import { IOptionsForAddConnProps } from "../../src/session/doc/IOptionsForAddConnProps"; import { ImperativeConfig } from "../../../utilities"; import { ConfigUtils } from "../../../config/src/ConfigUtils"; - - -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", () => { - afterEach(() => { jest.clearAllMocks(); }); - it("authenticate with user and pass", async() => { + it("authenticate with user and pass", async () => { const initialSessCfg = { rejectUnauthorized: true, }; @@ -51,11 +77,13 @@ describe("ConnectionPropsForSessCfg tests", () => { host: "SomeHost", port: 11, user: "FakeUser", - password: "FakePassword" + password: "FakePassword", }; - const sessCfgWithConnProps = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args - ); + const sessCfgWithConnProps = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args + ); expect(sessCfgWithConnProps.hostname).toBe("SomeHost"); expect(sessCfgWithConnProps.port).toBe(11); expect(sessCfgWithConnProps.user).toBe("FakeUser"); @@ -67,58 +95,68 @@ describe("ConnectionPropsForSessCfg tests", () => { expect(sessCfgWithConnProps.certKey).toBeUndefined(); }); - it("authenticate with user, pass, and tokenType to get token", async() => { + it("authenticate with user, pass, and tokenType to get token", async () => { const initialSessCfg = { hostname: "SomeHost", port: 11, - rejectUnauthorized: true + rejectUnauthorized: true, }; const args = { $0: "zowe", _: [""], user: "FakeUser", password: "FakePassword", - tokenType: SessConstants.TOKEN_TYPE_JWT + tokenType: SessConstants.TOKEN_TYPE_JWT, }; - const sessCfgWithConnProps = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args, {requestToken: true} - ); + const sessCfgWithConnProps = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args, + { requestToken: true } + ); expect(sessCfgWithConnProps.hostname).toBe("SomeHost"); expect(sessCfgWithConnProps.user).toBe("FakeUser"); expect(sessCfgWithConnProps.password).toBe("FakePassword"); expect(sessCfgWithConnProps.type).toBe(SessConstants.AUTH_TYPE_TOKEN); - expect(sessCfgWithConnProps.tokenType).toBe(SessConstants.TOKEN_TYPE_JWT); + expect(sessCfgWithConnProps.tokenType).toBe( + SessConstants.TOKEN_TYPE_JWT + ); expect(sessCfgWithConnProps.tokenValue).toBeUndefined(); expect(sessCfgWithConnProps.cert).toBeUndefined(); expect(sessCfgWithConnProps.certKey).toBeUndefined(); }); - it("authenticate with user, pass, and *NO* tokenType to get token", async() => { + it("authenticate with user, pass, and *NO* tokenType to get token", async () => { const initialSessCfg = { hostname: "SomeHost", port: 11, - rejectUnauthorized: true + rejectUnauthorized: true, }; const args = { $0: "zowe", _: [""], user: "FakeUser", - password: "FakePassword" + password: "FakePassword", }; - const sessCfgWithConnProps = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args, {requestToken: true} - ); + const sessCfgWithConnProps = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args, + { requestToken: true } + ); expect(sessCfgWithConnProps.hostname).toBe("SomeHost"); expect(sessCfgWithConnProps.user).toBe("FakeUser"); expect(sessCfgWithConnProps.password).toBe("FakePassword"); expect(sessCfgWithConnProps.type).toBe(SessConstants.AUTH_TYPE_TOKEN); - expect(sessCfgWithConnProps.tokenType).toBe(SessConstants.TOKEN_TYPE_JWT); + expect(sessCfgWithConnProps.tokenType).toBe( + SessConstants.TOKEN_TYPE_JWT + ); expect(sessCfgWithConnProps.tokenValue).toBeUndefined(); expect(sessCfgWithConnProps.cert).toBeUndefined(); expect(sessCfgWithConnProps.certKey).toBeUndefined(); }); - it("authenticate with token value", async() => { + it("authenticate with token value", async () => { const initialSessCfg = { hostname: "SomeHost", port: 11, @@ -129,9 +167,11 @@ describe("ConnectionPropsForSessCfg tests", () => { _: [""], tokenValue: "FakeToken", }; - const sessCfgWithConnProps = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args - ); + const sessCfgWithConnProps = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args + ); expect(sessCfgWithConnProps.hostname).toBe("SomeHost"); expect(sessCfgWithConnProps.tokenValue).toBe("FakeToken"); expect(sessCfgWithConnProps.type).toBe(SessConstants.AUTH_TYPE_BEARER); @@ -141,48 +181,56 @@ describe("ConnectionPropsForSessCfg tests", () => { expect(sessCfgWithConnProps.certKey).toBeUndefined(); }); - it("authenticate with token value and token type", async() => { + it("authenticate with token value and token type", async () => { const initialSessCfg = { hostname: "SomeHost", port: 11, - rejectUnauthorized: true + rejectUnauthorized: true, }; const args = { $0: "zowe", _: [""], tokenValue: "FakeToken", - tokenType: SessConstants.TOKEN_TYPE_LTPA + tokenType: SessConstants.TOKEN_TYPE_LTPA, }; - const sessCfgWithConnProps = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args - ); + const sessCfgWithConnProps = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args + ); expect(sessCfgWithConnProps.hostname).toBe("SomeHost"); expect(sessCfgWithConnProps.tokenValue).toBe("FakeToken"); expect(sessCfgWithConnProps.type).toBe(SessConstants.AUTH_TYPE_TOKEN); - expect(sessCfgWithConnProps.tokenType).toBe(SessConstants.TOKEN_TYPE_LTPA); + expect(sessCfgWithConnProps.tokenType).toBe( + SessConstants.TOKEN_TYPE_LTPA + ); expect(sessCfgWithConnProps.user).toBeUndefined(); expect(sessCfgWithConnProps.password).toBeUndefined(); expect(sessCfgWithConnProps.cert).toBeUndefined(); expect(sessCfgWithConnProps.certKey).toBeUndefined(); }); - it("authenticate with certFile and certKeyFile", async() => { + it("authenticate with certFile and certKeyFile", async () => { const initialSessCfg = { hostname: "SomeHost", port: 11, - rejectUnauthorized: true + rejectUnauthorized: true, }; const args = { $0: "zowe", _: [""], certFile: certFilePath, - certKeyFile: certKeyFilePath + certKeyFile: certKeyFilePath, }; - const sessCfgWithConnProps = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args - ); + const sessCfgWithConnProps = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args + ); expect(sessCfgWithConnProps.hostname).toBe("SomeHost"); - expect(sessCfgWithConnProps.type).toBe(SessConstants.AUTH_TYPE_CERT_PEM); + expect(sessCfgWithConnProps.type).toBe( + SessConstants.AUTH_TYPE_CERT_PEM + ); expect(sessCfgWithConnProps.cert).toBe(certFilePath); expect(sessCfgWithConnProps.certKey).toBe(certKeyFilePath); expect(sessCfgWithConnProps.user).toBeUndefined(); @@ -191,11 +239,11 @@ describe("ConnectionPropsForSessCfg tests", () => { expect(sessCfgWithConnProps.tokenValue).toBeUndefined(); }); - it("ignore token and cert if unsupported auth types and authenticate with user and pass", async() => { + it("ignore token and cert if unsupported auth types and authenticate with user and pass", async () => { const initialSessCfg = { hostname: "SomeHost", port: 11, - rejectUnauthorized: true + rejectUnauthorized: true, }; const args = { $0: "zowe", @@ -203,15 +251,18 @@ describe("ConnectionPropsForSessCfg tests", () => { cert: "fakeCert", certKey: "fakeCertKey", tokenType: SessConstants.TOKEN_TYPE_JWT, - tokenValue: "fakeToken" + tokenValue: "fakeToken", }; const fakePromptFn = jest.fn().mockReturnValue({ - "user": "FakeUser", - "password": "FakePassword" + user: "FakeUser", + password: "FakePassword", }); - const sessCfgWithConnProps = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args, {getValuesBack: fakePromptFn, supportedAuthTypes: ["basic"]} - ); + const sessCfgWithConnProps = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args, + { getValuesBack: fakePromptFn, supportedAuthTypes: ["basic"] } + ); expect(fakePromptFn).toHaveBeenCalledWith(["user", "password"]); expect(sessCfgWithConnProps.hostname).toBe("SomeHost"); expect(sessCfgWithConnProps.user).toBe("FakeUser"); @@ -219,11 +270,11 @@ describe("ConnectionPropsForSessCfg tests", () => { expect(sessCfgWithConnProps.type).toBe(SessConstants.AUTH_TYPE_BASIC); }); - it("not set tokenValue if user and pass are defined", async() => { + it("not set tokenValue if user and pass are defined", async () => { const initialSessCfg = { hostname: "SomeHost", port: 11, - rejectUnauthorized: true + rejectUnauthorized: true, }; const args = { $0: "zowe", @@ -231,11 +282,13 @@ describe("ConnectionPropsForSessCfg tests", () => { user: "FakeUser", password: "FakePassword", tokenType: SessConstants.TOKEN_TYPE_JWT, - tokenValue: "FakeToken" + tokenValue: "FakeToken", }; - const sessCfgWithConnProps = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args - ); + const sessCfgWithConnProps = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args + ); expect(sessCfgWithConnProps.hostname).toBe("SomeHost"); expect(sessCfgWithConnProps.user).toBe("FakeUser"); expect(sessCfgWithConnProps.password).toBe("FakePassword"); @@ -246,20 +299,23 @@ describe("ConnectionPropsForSessCfg tests", () => { expect(sessCfgWithConnProps.certKey).toBeUndefined(); }); - it("not prompt when asked not to prompt", async() => { + it("not prompt when asked not to prompt", async () => { const initialSessCfg = { hostname: "SomeHost", port: 11, - rejectUnauthorized: true + rejectUnauthorized: true, }; const args = { $0: "zowe", - _: [""] + _: [""], }; - const sessCfgWithConnProps: ISession = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args, {doPrompting: false} - ); + const sessCfgWithConnProps: ISession = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args, + { doPrompting: false } + ); expect(sessCfgWithConnProps.type).toBe(SessConstants.AUTH_TYPE_BASIC); expect(sessCfgWithConnProps.user).toBeUndefined(); expect(sessCfgWithConnProps.password).toBeUndefined(); @@ -269,32 +325,37 @@ describe("ConnectionPropsForSessCfg tests", () => { expect(sessCfgWithConnProps.certKey).toBeUndefined(); }); - it("override with a different command line name", async() => { + it("override with a different command line name", async () => { const passFromPrompt = "somePass"; const initialSessCfg: extendedSession = { hostname: "SomeHost", port: 11, user: "FakeUser", password: "somePass", - rejectUnauthorized: true + rejectUnauthorized: true, }; const args = { $0: "zowe", _: [""], - someKeyOther: "somekeyvalue" - }; - const overrides: IOverridePromptConnProps[] = [{ - propertyName: "someKey", - argumentName: "someKeyOther", - propertiesOverridden: [ - "password", - "tokenType", - "tokenValue", - "cert", - "certKey" - ] - }]; - const mockClientPrompt = jest.spyOn(ConnectionPropsForSessCfg as any, "clientPrompt"); + someKeyOther: "somekeyvalue", + }; + const overrides: IOverridePromptConnProps[] = [ + { + propertyName: "someKey", + argumentName: "someKeyOther", + propertiesOverridden: [ + "password", + "tokenType", + "tokenValue", + "cert", + "certKey", + ], + }, + ]; + const mockClientPrompt = jest.spyOn( + ConnectionPropsForSessCfg as any, + "clientPrompt" + ); // command handler prompt method (CLI versus SDK-based prompting) const commandHandlerPrompt = jest.fn(() => { return Promise.resolve(passFromPrompt); @@ -303,13 +364,20 @@ describe("ConnectionPropsForSessCfg tests", () => { const parms = { response: { console: { - prompt: commandHandlerPrompt - } - } + prompt: commandHandlerPrompt, + }, + }, }; - const sessCfgWithConnProps: extendedSession = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args, {doPrompting: true, propertyOverrides: overrides, parms: (parms as any)} - ); + const sessCfgWithConnProps: extendedSession = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args, + { + doPrompting: true, + propertyOverrides: overrides, + parms: parms as any, + } + ); expect(sessCfgWithConnProps.type).toBe(SessConstants.AUTH_TYPE_BASIC); expect(commandHandlerPrompt).not.toHaveBeenCalled(); // we are only testing that we call an already tested prompt method if in CLI mode expect(mockClientPrompt).not.toHaveBeenCalled(); @@ -322,31 +390,36 @@ describe("ConnectionPropsForSessCfg tests", () => { expect(sessCfgWithConnProps.certKey).toBeUndefined(); }); - it("override a session value when an override is specified on the command line", async() => { + it("override a session value when an override is specified on the command line", async () => { const passFromPrompt = "somePass"; const initialSessCfg: extendedSession = { hostname: "SomeHost", port: 11, user: "FakeUser", password: "somePass", - rejectUnauthorized: true + rejectUnauthorized: true, }; const args = { $0: "zowe", _: [""], - someKey: "somekeyvalue" - }; - const overrides: IOverridePromptConnProps[] = [{ - propertyName: "someKey", - propertiesOverridden: [ - "password", - "tokenType", - "tokenValue", - "cert", - "certKey" - ] - }]; - const mockClientPrompt = jest.spyOn(ConnectionPropsForSessCfg as any, "clientPrompt"); + someKey: "somekeyvalue", + }; + const overrides: IOverridePromptConnProps[] = [ + { + propertyName: "someKey", + propertiesOverridden: [ + "password", + "tokenType", + "tokenValue", + "cert", + "certKey", + ], + }, + ]; + const mockClientPrompt = jest.spyOn( + ConnectionPropsForSessCfg as any, + "clientPrompt" + ); // command handler prompt method (CLI versus SDK-based prompting) const commandHandlerPrompt = jest.fn(() => { return Promise.resolve(passFromPrompt); @@ -355,13 +428,20 @@ describe("ConnectionPropsForSessCfg tests", () => { const parms = { response: { console: { - prompt: commandHandlerPrompt - } - } + prompt: commandHandlerPrompt, + }, + }, }; - const sessCfgWithConnProps: extendedSession = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args, {doPrompting: true, propertyOverrides: overrides, parms: (parms as any)} - ); + const sessCfgWithConnProps: extendedSession = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args, + { + doPrompting: true, + propertyOverrides: overrides, + parms: parms as any, + } + ); expect(sessCfgWithConnProps.type).toBe(SessConstants.AUTH_TYPE_BASIC); expect(commandHandlerPrompt).not.toHaveBeenCalled(); // we are only testing that we call an already tested prompt method if in CLI mode expect(mockClientPrompt).not.toHaveBeenCalled(); @@ -374,7 +454,7 @@ describe("ConnectionPropsForSessCfg tests", () => { expect(sessCfgWithConnProps.certKey).toBeUndefined(); }); - it("override a session value when an override is specified on the session", async() => { + it("override a session value when an override is specified on the session", async () => { const passFromPrompt = "somePass"; const initialSessCfg: extendedSession = { hostname: "SomeHost", @@ -382,23 +462,28 @@ describe("ConnectionPropsForSessCfg tests", () => { user: "FakeUser", password: "somePass", someKey: "somekeyvalue", - rejectUnauthorized: true + rejectUnauthorized: true, }; const args = { $0: "zowe", - _: [""] - }; - const overrides: IOverridePromptConnProps[] = [{ - propertyName: "someKey", - propertiesOverridden: [ - "password", - "tokenType", - "tokenValue", - "cert", - "certKey" - ] - }]; - const mockClientPrompt = jest.spyOn(ConnectionPropsForSessCfg as any, "clientPrompt"); + _: [""], + }; + const overrides: IOverridePromptConnProps[] = [ + { + propertyName: "someKey", + propertiesOverridden: [ + "password", + "tokenType", + "tokenValue", + "cert", + "certKey", + ], + }, + ]; + const mockClientPrompt = jest.spyOn( + ConnectionPropsForSessCfg as any, + "clientPrompt" + ); // command handler prompt method (CLI versus SDK-based prompting) const commandHandlerPrompt = jest.fn(() => { return Promise.resolve(passFromPrompt); @@ -407,13 +492,20 @@ describe("ConnectionPropsForSessCfg tests", () => { const parms = { response: { console: { - prompt: commandHandlerPrompt - } - } + prompt: commandHandlerPrompt, + }, + }, }; - const sessCfgWithConnProps: extendedSession = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args, {doPrompting: true, propertyOverrides: overrides, parms: (parms as any)} - ); + const sessCfgWithConnProps: extendedSession = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args, + { + doPrompting: true, + propertyOverrides: overrides, + parms: parms as any, + } + ); expect(sessCfgWithConnProps.type).toBe(SessConstants.AUTH_TYPE_BASIC); expect(commandHandlerPrompt).not.toHaveBeenCalled(); // we are only testing that we call an already tested prompt method if in CLI mode expect(mockClientPrompt).not.toHaveBeenCalled(); @@ -426,30 +518,35 @@ describe("ConnectionPropsForSessCfg tests", () => { expect(sessCfgWithConnProps.certKey).toBeUndefined(); }); - it("not prompt when an override is specified on the command line", async() => { + it("not prompt when an override is specified on the command line", async () => { const passFromPrompt = "somePass"; const initialSessCfg: extendedSession = { hostname: "SomeHost", port: 11, user: "FakeUser", - rejectUnauthorized: true + rejectUnauthorized: true, }; const args = { $0: "zowe", _: [""], - someKey: "somekeyvalue" - }; - const overrides: IOverridePromptConnProps[] = [{ - propertyName: "someKey", - propertiesOverridden: [ - "password", - "tokenType", - "tokenValue", - "cert", - "certKey" - ] - }]; - const mockClientPrompt = jest.spyOn(ConnectionPropsForSessCfg as any, "clientPrompt"); + someKey: "somekeyvalue", + }; + const overrides: IOverridePromptConnProps[] = [ + { + propertyName: "someKey", + propertiesOverridden: [ + "password", + "tokenType", + "tokenValue", + "cert", + "certKey", + ], + }, + ]; + const mockClientPrompt = jest.spyOn( + ConnectionPropsForSessCfg as any, + "clientPrompt" + ); // command handler prompt method (CLI versus SDK-based prompting) const commandHandlerPrompt = jest.fn(() => { return Promise.resolve(passFromPrompt); @@ -458,13 +555,20 @@ describe("ConnectionPropsForSessCfg tests", () => { const parms = { response: { console: { - prompt: commandHandlerPrompt - } - } + prompt: commandHandlerPrompt, + }, + }, }; - const sessCfgWithConnProps: extendedSession = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args, {doPrompting: true, propertyOverrides: overrides, parms: (parms as any)} - ); + const sessCfgWithConnProps: extendedSession = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args, + { + doPrompting: true, + propertyOverrides: overrides, + parms: parms as any, + } + ); expect(sessCfgWithConnProps.type).toBe(SessConstants.AUTH_TYPE_BASIC); expect(commandHandlerPrompt).not.toHaveBeenCalled(); // we are only testing that we call an already tested prompt method if in CLI mode expect(mockClientPrompt).not.toHaveBeenCalled(); @@ -477,30 +581,35 @@ describe("ConnectionPropsForSessCfg tests", () => { expect(sessCfgWithConnProps.certKey).toBeUndefined(); }); - it("not prompt when an override is specified on the session", async() => { + it("not prompt when an override is specified on the session", async () => { const passFromPrompt = "somePass"; const initialSessCfg: extendedSession = { hostname: "SomeHost", port: 11, user: "FakeUser", rejectUnauthorized: true, - someKey: "somekeyvalue" + someKey: "somekeyvalue", }; const args = { $0: "zowe", - _: [""] - }; - const overrides: IOverridePromptConnProps[] = [{ - propertyName: "someKey", - propertiesOverridden: [ - "password", - "tokenType", - "tokenValue", - "cert", - "certKey" - ] - }]; - const mockClientPrompt = jest.spyOn(ConnectionPropsForSessCfg as any, "clientPrompt"); + _: [""], + }; + const overrides: IOverridePromptConnProps[] = [ + { + propertyName: "someKey", + propertiesOverridden: [ + "password", + "tokenType", + "tokenValue", + "cert", + "certKey", + ], + }, + ]; + const mockClientPrompt = jest.spyOn( + ConnectionPropsForSessCfg as any, + "clientPrompt" + ); // command handler prompt method (CLI versus SDK-based prompting) const commandHandlerPrompt = jest.fn(() => { return Promise.resolve(passFromPrompt); @@ -509,13 +618,20 @@ describe("ConnectionPropsForSessCfg tests", () => { const parms = { response: { console: { - prompt: commandHandlerPrompt - } - } + prompt: commandHandlerPrompt, + }, + }, }; - const sessCfgWithConnProps: extendedSession = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args, {doPrompting: true, propertyOverrides: overrides, parms: (parms as any)} - ); + const sessCfgWithConnProps: extendedSession = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args, + { + doPrompting: true, + propertyOverrides: overrides, + parms: parms as any, + } + ); expect(sessCfgWithConnProps.type).toBe(SessConstants.AUTH_TYPE_BASIC); expect(commandHandlerPrompt).not.toHaveBeenCalled(); // we are only testing that we call an already tested prompt method if in CLI mode expect(mockClientPrompt).not.toHaveBeenCalled(); @@ -528,29 +644,34 @@ describe("ConnectionPropsForSessCfg tests", () => { expect(sessCfgWithConnProps.certKey).toBeUndefined(); }); - it("should prompt when an override is specified but is not present", async() => { + it("should prompt when an override is specified but is not present", async () => { const passFromPrompt = "somePass"; const initialSessCfg: extendedSession = { hostname: "SomeHost", port: 11, user: "FakeUser", - rejectUnauthorized: true + rejectUnauthorized: true, }; const args = { $0: "zowe", - _: [""] - }; - const overrides: IOverridePromptConnProps[] = [{ - propertyName: "someKey", - propertiesOverridden: [ - "password", - "tokenType", - "tokenValue", - "cert", - "certKey" - ] - }]; - const mockClientPrompt = jest.spyOn(ConnectionPropsForSessCfg as any, "clientPrompt"); + _: [""], + }; + const overrides: IOverridePromptConnProps[] = [ + { + propertyName: "someKey", + propertiesOverridden: [ + "password", + "tokenType", + "tokenValue", + "cert", + "certKey", + ], + }, + ]; + const mockClientPrompt = jest.spyOn( + ConnectionPropsForSessCfg as any, + "clientPrompt" + ); // command handler prompt method (CLI versus SDK-based prompting) const commandHandlerPrompt = jest.fn(() => { return Promise.resolve(passFromPrompt); @@ -559,13 +680,20 @@ describe("ConnectionPropsForSessCfg tests", () => { const parms = { response: { console: { - prompt: commandHandlerPrompt - } - } + prompt: commandHandlerPrompt, + }, + }, }; - const sessCfgWithConnProps: extendedSession = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args, {doPrompting: true, propertyOverrides: overrides, parms: (parms as any)} - ); + const sessCfgWithConnProps: extendedSession = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args, + { + doPrompting: true, + propertyOverrides: overrides, + parms: parms as any, + } + ); expect(sessCfgWithConnProps.type).toBe(SessConstants.AUTH_TYPE_BASIC); expect(commandHandlerPrompt).toHaveBeenCalled(); // we are only testing that we call an already tested prompt method if in CLI mode expect((mockClientPrompt.mock.calls[0][1] as any).parms).toBe(parms); @@ -578,31 +706,36 @@ describe("ConnectionPropsForSessCfg tests", () => { expect(sessCfgWithConnProps.certKey).toBeUndefined(); }); - it("not prompt when an override is specified and should prioritize the argument value", async() => { + it("not prompt when an override is specified and should prioritize the argument value", async () => { const passFromPrompt = "somePass"; const initialSessCfg: extendedSession = { hostname: "SomeHost", port: 11, user: "FakeUser", rejectUnauthorized: true, - someKey: "somekeyvalue" + someKey: "somekeyvalue", }; const args = { $0: "zowe", _: [""], - someKey: "someotherkeyvalue" - }; - const overrides: IOverridePromptConnProps[] = [{ - propertyName: "someKey", - propertiesOverridden: [ - "password", - "tokenType", - "tokenValue", - "cert", - "certKey" - ] - }]; - const mockClientPrompt = jest.spyOn(ConnectionPropsForSessCfg as any, "clientPrompt"); + someKey: "someotherkeyvalue", + }; + const overrides: IOverridePromptConnProps[] = [ + { + propertyName: "someKey", + propertiesOverridden: [ + "password", + "tokenType", + "tokenValue", + "cert", + "certKey", + ], + }, + ]; + const mockClientPrompt = jest.spyOn( + ConnectionPropsForSessCfg as any, + "clientPrompt" + ); // command handler prompt method (CLI versus SDK-based prompting) const commandHandlerPrompt = jest.fn(() => { return Promise.resolve(passFromPrompt); @@ -611,13 +744,20 @@ describe("ConnectionPropsForSessCfg tests", () => { const parms = { response: { console: { - prompt: commandHandlerPrompt - } - } + prompt: commandHandlerPrompt, + }, + }, }; - const sessCfgWithConnProps: extendedSession = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args, {doPrompting: true, propertyOverrides: overrides, parms: (parms as any)} - ); + const sessCfgWithConnProps: extendedSession = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args, + { + doPrompting: true, + propertyOverrides: overrides, + parms: parms as any, + } + ); expect(sessCfgWithConnProps.type).toBe(SessConstants.AUTH_TYPE_BASIC); expect(commandHandlerPrompt).not.toHaveBeenCalled(); // we are only testing that we call an already tested prompt method if in CLI mode expect(mockClientPrompt).not.toHaveBeenCalled(); @@ -630,21 +770,24 @@ describe("ConnectionPropsForSessCfg tests", () => { expect(sessCfgWithConnProps.certKey).toBeUndefined(); }); - it("get user name from prompt from daemon client", async() => { + it("get user name from prompt from daemon client", async () => { const userFromPrompt = "FakeUser"; const passFromArgs = "FakePassword"; - const mockClientPrompt = jest.spyOn(ConnectionPropsForSessCfg as any, "clientPrompt"); + const mockClientPrompt = jest.spyOn( + ConnectionPropsForSessCfg as any, + "clientPrompt" + ); const initialSessCfg = { hostname: "SomeHost", port: 11, - rejectUnauthorized: true + rejectUnauthorized: true, }; const args = { $0: "zowe", _: [""], - password: passFromArgs + password: passFromArgs, }; // command handler prompt method (CLI versus SDK-based prompting) @@ -656,22 +799,25 @@ describe("ConnectionPropsForSessCfg tests", () => { const parms = { response: { console: { - prompt: commandHandlerPrompt - } - } + prompt: commandHandlerPrompt, + }, + }, }; - const sessCfgWithConnProps: ISession = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args, { - parms: parms as any // treat this as a CLI-based prompt - } - ); + const sessCfgWithConnProps: ISession = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args, + { + parms: parms as any, // treat this as a CLI-based prompt + } + ); expect(commandHandlerPrompt).toHaveBeenCalled(); // we are only testing that we call an already tested prompt method if in CLI mode - expect((mockClientPrompt.mock.calls[0][1] as any).parms).toBe(parms); // toBe is important here, parms object must be same as original + expect((mockClientPrompt.mock.calls[0][1] as any).parms).toBe(parms); // toBe is important here, parms object must be same as original }); - it("get user name from prompt", async() => { + it("get user name from prompt", async () => { const userFromPrompt = "FakeUser"; const passFromArgs = "FakePassword"; @@ -685,17 +831,19 @@ describe("ConnectionPropsForSessCfg tests", () => { const initialSessCfg = { hostname: "SomeHost", port: 11, - rejectUnauthorized: true + rejectUnauthorized: true, }; const args = { $0: "zowe", _: [""], - password: passFromArgs + password: passFromArgs, }; - const sessCfgWithConnProps: ISession = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args - ); + const sessCfgWithConnProps: ISession = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args + ); CliUtils.sleep = sleepReal; CliUtils.readPrompt = readPromptReal; @@ -708,7 +856,7 @@ describe("ConnectionPropsForSessCfg tests", () => { expect(sessCfgWithConnProps.certKey).toBeUndefined(); }); - it("get password from prompt", async() => { + it("get password from prompt", async () => { const userFromArgs = "FakeUser"; const passFromPrompt = "FakePassword"; @@ -722,17 +870,19 @@ describe("ConnectionPropsForSessCfg tests", () => { const initialSessCfg = { hostname: "SomeHost", port: 11, - rejectUnauthorized: true + rejectUnauthorized: true, }; const args = { $0: "zowe", _: [""], - user: userFromArgs + user: userFromArgs, }; - const sessCfgWithConnProps: ISession = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args - ); + const sessCfgWithConnProps: ISession = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args + ); CliUtils.sleep = sleepReal; CliUtils.readPrompt = readPromptReal; @@ -745,7 +895,7 @@ describe("ConnectionPropsForSessCfg tests", () => { expect(sessCfgWithConnProps.certKey).toBeUndefined(); }); - it("get host name from prompt", async() => { + it("get host name from prompt", async () => { const hostFromPrompt = "FakeHost"; const portFromArgs = 11; const userFromArgs = "FakeUser"; @@ -766,12 +916,14 @@ describe("ConnectionPropsForSessCfg tests", () => { _: [""], port: portFromArgs, user: userFromArgs, - password: passFromArgs + password: passFromArgs, }; - const sessCfgWithConnProps: ISession = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args - ); + const sessCfgWithConnProps: ISession = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args + ); CliUtils.sleep = sleepReal; CliUtils.readPrompt = readPromptReal; @@ -785,7 +937,7 @@ describe("ConnectionPropsForSessCfg tests", () => { expect(sessCfgWithConnProps.certKey).toBeUndefined(); }); - it("get port from prompt - string", async() => { + it("get port from prompt - string", async () => { const hostFromArgs = "FakeHost"; const portFromPrompt = "11"; const userFromArgs = "FakeUser"; @@ -799,19 +951,21 @@ describe("ConnectionPropsForSessCfg tests", () => { }); const initialSessCfg = { - rejectUnauthorized: true + rejectUnauthorized: true, }; const args = { $0: "zowe", _: [""], host: hostFromArgs, user: userFromArgs, - password: passFromArgs + password: passFromArgs, }; - const sessCfgWithConnProps: ISession = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args - ); + const sessCfgWithConnProps: ISession = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args + ); CliUtils.sleep = sleepReal; CliUtils.readPrompt = readPromptReal; @@ -824,7 +978,7 @@ describe("ConnectionPropsForSessCfg tests", () => { expect(sessCfgWithConnProps.tokenValue).toBeUndefined(); }); - it("get port from prompt - number", async() => { + it("get port from prompt - number", async () => { const hostFromArgs = "FakeHost"; const portFromPrompt = 11; const userFromArgs = "FakeUser"; @@ -836,24 +990,29 @@ describe("ConnectionPropsForSessCfg tests", () => { CliUtils.readPrompt = jest.fn(() => { return Promise.resolve(portFromPrompt.toString()); }); - jest.spyOn(ConnectionPropsForSessCfg as any, "loadSchemaForSessCfgProps").mockReturnValueOnce({ - port: { type: "number" } + jest.spyOn( + ConnectionPropsForSessCfg as any, + "loadSchemaForSessCfgProps" + ).mockReturnValueOnce({ + port: { type: "number" }, }); const initialSessCfg = { - rejectUnauthorized: true + rejectUnauthorized: true, }; const args = { $0: "zowe", _: [""], host: hostFromArgs, user: userFromArgs, - password: passFromArgs + password: passFromArgs, }; - const sessCfgWithConnProps: ISession = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args - ); + const sessCfgWithConnProps: ISession = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args + ); CliUtils.sleep = sleepReal; CliUtils.readPrompt = readPromptReal; @@ -866,7 +1025,7 @@ describe("ConnectionPropsForSessCfg tests", () => { expect(sessCfgWithConnProps.tokenValue).toBeUndefined(); }); - it("get port from prompt - zero", async() => { + it("get port from prompt - zero", async () => { const hostFromArgs = "FakeHost"; const portFromArgs = 0; const portFromPrompt = 11; @@ -879,12 +1038,15 @@ describe("ConnectionPropsForSessCfg tests", () => { CliUtils.readPrompt = jest.fn(() => { return Promise.resolve(portFromPrompt.toString()); }); - jest.spyOn(ConnectionPropsForSessCfg as any, "loadSchemaForSessCfgProps").mockReturnValueOnce({ - port: { type: "number" } + jest.spyOn( + ConnectionPropsForSessCfg as any, + "loadSchemaForSessCfgProps" + ).mockReturnValueOnce({ + port: { type: "number" }, }); const initialSessCfg = { - rejectUnauthorized: true + rejectUnauthorized: true, }; const args = { $0: "zowe", @@ -892,12 +1054,14 @@ describe("ConnectionPropsForSessCfg tests", () => { host: hostFromArgs, port: portFromArgs, user: userFromArgs, - password: passFromArgs + password: passFromArgs, }; - const sessCfgWithConnProps: ISession = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args - ); + const sessCfgWithConnProps: ISession = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args + ); CliUtils.sleep = sleepReal; CliUtils.readPrompt = readPromptReal; @@ -910,7 +1074,7 @@ describe("ConnectionPropsForSessCfg tests", () => { expect(sessCfgWithConnProps.tokenValue).toBeUndefined(); }); - it("get host name from prompt with custom service description", async() => { + it("get host name from prompt with custom service description", async () => { const hostFromPrompt = "FakeHost"; const portFromArgs = 11; const userFromArgs = "FakeUser"; @@ -933,12 +1097,15 @@ describe("ConnectionPropsForSessCfg tests", () => { _: [""], port: portFromArgs, user: userFromArgs, - password: passFromArgs + password: passFromArgs, }; - const sessCfgWithConnProps: ISession = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args, { serviceDescription: "my cool service" } - ); + const sessCfgWithConnProps: ISession = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args, + { serviceDescription: "my cool service" } + ); CliUtils.sleep = sleepReal; CliUtils.readPrompt = readPromptReal; @@ -951,7 +1118,7 @@ describe("ConnectionPropsForSessCfg tests", () => { expect(sessCfgWithConnProps.tokenValue).toBeUndefined(); }); - it("get port from prompt with custom service description", async() => { + it("get port from prompt with custom service description", async () => { const hostFromArgs = "FakeHost"; const portFromPrompt = 11; const userFromArgs = "FakeUser"; @@ -965,24 +1132,30 @@ describe("ConnectionPropsForSessCfg tests", () => { questionText = text; return Promise.resolve(portFromPrompt.toString()); }); - jest.spyOn(ConnectionPropsForSessCfg as any, "loadSchemaForSessCfgProps").mockReturnValueOnce({ - port: { type: "number" } + jest.spyOn( + ConnectionPropsForSessCfg as any, + "loadSchemaForSessCfgProps" + ).mockReturnValueOnce({ + port: { type: "number" }, }); const initialSessCfg = { - rejectUnauthorized: true + rejectUnauthorized: true, }; const args = { $0: "zowe", _: [""], host: hostFromArgs, user: userFromArgs, - password: passFromArgs + password: passFromArgs, }; - const sessCfgWithConnProps: ISession = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args, { serviceDescription: "my cool service" } - ); + const sessCfgWithConnProps: ISession = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args, + { serviceDescription: "my cool service" } + ); CliUtils.sleep = sleepReal; CliUtils.readPrompt = readPromptReal; @@ -998,7 +1171,7 @@ describe("ConnectionPropsForSessCfg tests", () => { expect(sessCfgWithConnProps.certKey).toBeUndefined(); }); - it("get host name from prompt with hidden text - service profile", async() => { + it("get host name from prompt with hidden text - service profile", async () => { const hostFromPrompt = "FakeHost"; const portFromArgs = 11; const userFromArgs = "FakeUser"; @@ -1014,7 +1187,7 @@ describe("ConnectionPropsForSessCfg tests", () => { _: [""], port: portFromArgs, user: userFromArgs, - password: passFromArgs + password: passFromArgs, }; // command handler prompt method (CLI versus SDK-based prompting) @@ -1029,28 +1202,34 @@ describe("ConnectionPropsForSessCfg tests", () => { arguments: {}, response: { console: { - prompt: commandHandlerPrompt - } - } + prompt: commandHandlerPrompt, + }, + }, }; - jest.spyOn(ConfigAutoStore, "findActiveProfile").mockReturnValueOnce(["fruit", "mango"]); + jest.spyOn(ConfigAutoStore, "findActiveProfile").mockReturnValueOnce([ + "fruit", + "mango", + ]); await setupConfigToLoad({ profiles: { mango: { type: "fruit", properties: {}, - secure: ["host"] - } + secure: ["host"], + }, }, - defaults: { fruit: "mango" } + defaults: { fruit: "mango" }, }); - const sessCfgWithConnProps: ISession = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args, { - parms: parms as any // treat this as a CLI-based prompt - } - ); + const sessCfgWithConnProps: ISession = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args, + { + parms: parms as any, // treat this as a CLI-based prompt + } + ); expect(questionText).toContain("(will be hidden)"); expect(promptOpts.hideText).toBe(true); @@ -1062,7 +1241,7 @@ describe("ConnectionPropsForSessCfg tests", () => { expect(sessCfgWithConnProps.tokenValue).toBeUndefined(); }); - it("get host name from prompt with hidden text - base profile", async() => { + it("get host name from prompt with hidden text - base profile", async () => { const hostFromPrompt = "FakeHost"; const portFromArgs = 11; const userFromArgs = "FakeUser"; @@ -1078,7 +1257,7 @@ describe("ConnectionPropsForSessCfg tests", () => { _: [""], port: portFromArgs, user: userFromArgs, - password: passFromArgs + password: passFromArgs, }; // command handler prompt method (CLI versus SDK-based prompting) @@ -1093,32 +1272,38 @@ describe("ConnectionPropsForSessCfg tests", () => { arguments: {}, response: { console: { - prompt: commandHandlerPrompt - } - } + prompt: commandHandlerPrompt, + }, + }, }; - jest.spyOn(ConfigAutoStore, "findActiveProfile").mockReturnValueOnce(["fruit", "mango"]); + jest.spyOn(ConfigAutoStore, "findActiveProfile").mockReturnValueOnce([ + "fruit", + "mango", + ]); await setupConfigToLoad({ profiles: { mango: { type: "fruit", - properties: {} + properties: {}, }, fruit: { type: "base", properties: {}, - secure: ["host"] - } + secure: ["host"], + }, }, - defaults: { fruit: "mango", base: "fruit" } + defaults: { fruit: "mango", base: "fruit" }, }); - const sessCfgWithConnProps: ISession = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args, { - parms: parms as any // treat this as a CLI-based prompt - } - ); + const sessCfgWithConnProps: ISession = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args, + { + parms: parms as any, // treat this as a CLI-based prompt + } + ); expect(questionText).toContain("(will be hidden)"); expect(promptOpts.hideText).toBe(true); @@ -1130,7 +1315,7 @@ describe("ConnectionPropsForSessCfg tests", () => { expect(sessCfgWithConnProps.tokenValue).toBeUndefined(); }); - it("throws an error if user doesn't enter port as a number", async() => { + it("throws an error if user doesn't enter port as a number", async () => { const hostFromArgs = "FakeHost"; const portFromPrompt = "abcd"; const userFromArgs = "FakeUser"; @@ -1142,24 +1327,30 @@ describe("ConnectionPropsForSessCfg tests", () => { CliUtils.readPrompt = jest.fn(() => { return Promise.resolve(portFromPrompt); }); - jest.spyOn(ConnectionPropsForSessCfg as any, "loadSchemaForSessCfgProps").mockReturnValueOnce({ - port: { type: "number" } + jest.spyOn( + ConnectionPropsForSessCfg as any, + "loadSchemaForSessCfgProps" + ).mockReturnValueOnce({ + port: { type: "number" }, }); const initialSessCfg = { - rejectUnauthorized: true + rejectUnauthorized: true, }; const args = { $0: "zowe", _: [""], host: hostFromArgs, user: userFromArgs, - password: passFromArgs + password: passFromArgs, }; let theError; try { - await ConnectionPropsForSessCfg.addPropsOrPrompt(initialSessCfg, args); + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args + ); } catch (err) { theError = err; } @@ -1169,7 +1360,7 @@ describe("ConnectionPropsForSessCfg tests", () => { expect(theError.message).toBe("Specified port was not a number."); }); - it("timeout waiting for user name", async() => { + it("timeout waiting for user name", async () => { const sleepReal = CliUtils.sleep; CliUtils.sleep = jest.fn(); const readPromptReal = CliUtils.readPrompt; @@ -1178,20 +1369,22 @@ describe("ConnectionPropsForSessCfg tests", () => { const initialSessCfg = { hostname: "SomeHost", port: 11, - rejectUnauthorized: true + rejectUnauthorized: true, }; const args = { $0: "zowe", _: [""], - password: "FakePassword" + password: "FakePassword", }; let sessCfgWithConnProps: ISession; let caughtError; try { - sessCfgWithConnProps = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args - ); + sessCfgWithConnProps = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args + ); } catch (thrownError) { caughtError = thrownError; } @@ -1201,7 +1394,7 @@ describe("ConnectionPropsForSessCfg tests", () => { expect(caughtError.message).toBe("Timed out waiting for user."); }); - it("timeout waiting for password", async() => { + it("timeout waiting for password", async () => { const sleepReal = CliUtils.sleep; CliUtils.sleep = jest.fn(); const readPromptReal = CliUtils.readPrompt; @@ -1210,20 +1403,22 @@ describe("ConnectionPropsForSessCfg tests", () => { const initialSessCfg = { hostname: "SomeHost", port: 11, - rejectUnauthorized: true + rejectUnauthorized: true, }; const args = { $0: "zowe", _: [""], - user: "FakeUser" + user: "FakeUser", }; let sessCfgWithConnProps: ISession; let caughtError; try { - sessCfgWithConnProps = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args - ); + sessCfgWithConnProps = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args + ); } catch (thrownError) { caughtError = thrownError; } @@ -1233,7 +1428,7 @@ describe("ConnectionPropsForSessCfg tests", () => { expect(caughtError.message).toBe("Timed out waiting for password."); }); - it("timeout waiting for host name", async() => { + it("timeout waiting for host name", async () => { const sleepReal = CliUtils.sleep; CliUtils.sleep = jest.fn(); const readPromptReal = CliUtils.readPrompt; @@ -1247,15 +1442,17 @@ describe("ConnectionPropsForSessCfg tests", () => { _: [""], port: 11, user: "FakeUser", - password: "FakePassword" + password: "FakePassword", }; let sessCfgWithConnProps: ISession; let caughtError; try { - sessCfgWithConnProps = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args - ); + sessCfgWithConnProps = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args + ); } catch (thrownError) { caughtError = thrownError; } @@ -1265,7 +1462,7 @@ describe("ConnectionPropsForSessCfg tests", () => { expect(caughtError.message).toBe("Timed out waiting for hostname."); }); - it("timeout waiting for port number", async() => { + it("timeout waiting for port number", async () => { const sleepReal = CliUtils.sleep; CliUtils.sleep = jest.fn(); const readPromptReal = CliUtils.readPrompt; @@ -1279,15 +1476,17 @@ describe("ConnectionPropsForSessCfg tests", () => { _: [""], host: "SomeHost", user: "FakeUser", - password: "FakePassword" + password: "FakePassword", }; let sessCfgWithConnProps: ISession; let caughtError; try { - sessCfgWithConnProps = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args - ); + sessCfgWithConnProps = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args + ); } catch (thrownError) { caughtError = thrownError; } @@ -1299,7 +1498,8 @@ describe("ConnectionPropsForSessCfg tests", () => { it("should not log secure properties of session config", async () => { const mockLoggerDebug = jest.fn(); - const getImperativeLoggerSpy = jest.spyOn(Logger, "getImperativeLogger") + const getImperativeLoggerSpy = jest + .spyOn(Logger, "getImperativeLogger") .mockReturnValueOnce({ debug: mockLoggerDebug } as any); (ConnectionPropsForSessCfg as any).logSessCfg({ host: "SomeHost", @@ -1307,7 +1507,7 @@ describe("ConnectionPropsForSessCfg tests", () => { user: "FakeUser", password: "FakePassword", tokenType: SessConstants.TOKEN_TYPE_JWT, - tokenValue: "FakeToken" + tokenValue: "FakeToken", }); getImperativeLoggerSpy.mockRestore(); expect(mockLoggerDebug).toHaveBeenCalledTimes(1); @@ -1318,7 +1518,7 @@ describe("ConnectionPropsForSessCfg tests", () => { expect(logOutput).not.toContain("FakeToken"); }); - it("SSO CallBack with getValuesBack", async() => { + it("SSO CallBack with getValuesBack", async () => { const initialSessCfg = { rejectUnauthorized: true, }; @@ -1327,28 +1527,28 @@ describe("ConnectionPropsForSessCfg tests", () => { port: 11, user: "FakeUser", password: "FakePassword", - rejectUnauthorized: false + rejectUnauthorized: false, }; const args = { $0: "zowe", - _: [""] + _: [""], }; const fakeFunction = jest.fn((neededProps) => { for (const value of neededProps) { switch (value) { - case "hostname" : + case "hostname": neededProps[value] = fakeFunctionSessCfg.hostname; break; - case "port" : + case "port": neededProps[value] = fakeFunctionSessCfg.port; break; - case "user" : + case "user": neededProps[value] = fakeFunctionSessCfg.user; break; - case "password" : + case "password": neededProps[value] = fakeFunctionSessCfg.password; break; - case "rejectUnauthorized" : + case "rejectUnauthorized": neededProps[value] = initialSessCfg.rejectUnauthorized; break; default: @@ -1357,9 +1557,12 @@ describe("ConnectionPropsForSessCfg tests", () => { } return neededProps; }); - const sessCfgWithConnProps = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args, {getValuesBack: fakeFunction} - ); + const sessCfgWithConnProps = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args, + { getValuesBack: fakeFunction } + ); expect(sessCfgWithConnProps.hostname).toBe("SomeHost"); expect(sessCfgWithConnProps.port).toBe(11); expect(sessCfgWithConnProps.user).toBe("FakeUser"); @@ -1371,7 +1574,7 @@ describe("ConnectionPropsForSessCfg tests", () => { expect(sessCfgWithConnProps.certKey).toBeUndefined(); }); - it("SSO CallBack with getValuesBack and partial session config", async() => { + it("SSO CallBack with getValuesBack and partial session config", async () => { const initialSessCfg = { password: "FakePassword", rejectUnauthorized: true, @@ -1388,19 +1591,19 @@ describe("ConnectionPropsForSessCfg tests", () => { const fakeFunction = jest.fn((neededProps) => { for (const value of neededProps) { switch (value) { - case "hostname" : + case "hostname": neededProps[value] = fakeFunctionSessCfg.hostname; break; - case "port" : + case "port": neededProps[value] = fakeFunctionSessCfg.port; break; - case "user" : + case "user": neededProps[value] = args.user; break; - case "password" : + case "password": neededProps[value] = initialSessCfg.password; break; - case "rejectUnauthorized" : + case "rejectUnauthorized": neededProps[value] = initialSessCfg.rejectUnauthorized; break; default: @@ -1409,9 +1612,12 @@ describe("ConnectionPropsForSessCfg tests", () => { } return neededProps; }); - const sessCfgWithConnProps = await ConnectionPropsForSessCfg.addPropsOrPrompt( - initialSessCfg, args, {getValuesBack: fakeFunction} - ); + const sessCfgWithConnProps = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + initialSessCfg, + args, + { getValuesBack: fakeFunction } + ); expect(sessCfgWithConnProps.hostname).toBe("SomeHost"); expect(sessCfgWithConnProps.port).toBe(11); expect(sessCfgWithConnProps.user).toBe("FakeUser"); @@ -1422,20 +1628,83 @@ 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( + initialSessCfg, + args, + { + doPrompting: true, + propertyOverrides: overrides, + propsToPromptFor: [{name: "keyPassphrase",isGivenValueValid: string => true}], + parms: parms as any, + } + ); + expect((ConnectionPropsForSessCfg as any).secureSessCfgProps).toContain("keyPassphrase"); + }); describe("getValuesBack private function", () => { // pretend that console.log works, but put data into a variable let consoleMsgs = ""; - const connOpts:IOptionsForAddConnProps = { + const connOpts: IOptionsForAddConnProps = { parms: { response: { console: { log: jest.fn((logArgs) => { consoleMsgs += "\n" + logArgs; - }) - } - } - } + }), + }, + }, + }, } as any; let getValuesCallBack: any; @@ -1443,12 +1712,13 @@ describe("ConnectionPropsForSessCfg tests", () => { beforeEach(() => { // establish a callback function with our fake console.log - getValuesCallBack = ConnectionPropsForSessCfg["getValuesBack"](connOpts); + getValuesCallBack = + ConnectionPropsForSessCfg["getValuesBack"](connOpts); // pretend that clientPrompt returns an answer - clientPromptSpy = jest.spyOn(ConnectionPropsForSessCfg as any, "clientPrompt").mockResolvedValue( - Promise.resolve("Some fake answer") - ); + clientPromptSpy = jest + .spyOn(ConnectionPropsForSessCfg as any, "clientPrompt") + .mockResolvedValue(Promise.resolve("Some fake answer")); // clear log messages from last test consoleMsgs = ""; }); @@ -1464,17 +1734,23 @@ describe("ConnectionPropsForSessCfg tests", () => { configurable: true, get: jest.fn(() => { return { - exists: false + exists: false, }; - }) + }), }); // call the function that we want to test await getValuesCallBack(["hostname"]); - expect(consoleMsgs).toContain("No Zowe client configuration exists."); - expect(consoleMsgs).toContain("Therefore, you will be asked for the"); - expect(consoleMsgs).toContain("connection properties that are required to complete your command."); + expect(consoleMsgs).toContain( + "No Zowe client configuration exists." + ); + expect(consoleMsgs).toContain( + "Therefore, you will be asked for the" + ); + expect(consoleMsgs).toContain( + "connection properties that are required to complete your command." + ); }); it("should state that V1 profiles are not supported", async () => { @@ -1483,9 +1759,9 @@ describe("ConnectionPropsForSessCfg tests", () => { configurable: true, get: jest.fn(() => { return { - exists: false + exists: false, }; - }) + }), }); /* Pretend that we only have V1 profiles. @@ -1495,15 +1771,21 @@ describe("ConnectionPropsForSessCfg tests", () => { configurable: true, get: jest.fn(() => { return true; - }) + }), }); // call the function that we want to test await getValuesCallBack(["hostname"]); - expect(consoleMsgs).toContain("Only V1 profiles exist. V1 profiles are no longer supported. You should convert"); - expect(consoleMsgs).toContain("your V1 profiles to a newer Zowe client configuration. Therefore, you will be"); - expect(consoleMsgs).toContain("asked for the connection properties that are required to complete your command."); + expect(consoleMsgs).toContain( + "Only V1 profiles exist. V1 profiles are no longer supported. You should convert" + ); + expect(consoleMsgs).toContain( + "your V1 profiles to a newer Zowe client configuration. Therefore, you will be" + ); + expect(consoleMsgs).toContain( + "asked for the connection properties that are required to complete your command." + ); }); it("should state that connection properties are missing from config", async () => { @@ -1512,9 +1794,9 @@ describe("ConnectionPropsForSessCfg tests", () => { configurable: true, get: jest.fn(() => { return { - exists: true + exists: true, }; - }) + }), }); /* Pretend that we do not have any V1 profiles. @@ -1524,15 +1806,21 @@ describe("ConnectionPropsForSessCfg tests", () => { configurable: true, get: jest.fn(() => { return false; - }) + }), }); // call the function that we want to test await getValuesCallBack(["hostname"]); - expect(consoleMsgs).toContain("Some required connection properties have not been specified in your Zowe client"); - expect(consoleMsgs).toContain("configuration. Therefore, you will be asked for the connection properties that"); - expect(consoleMsgs).toContain("are required to complete your command."); + expect(consoleMsgs).toContain( + "Some required connection properties have not been specified in your Zowe client" + ); + expect(consoleMsgs).toContain( + "configuration. Therefore, you will be asked for the connection properties that" + ); + expect(consoleMsgs).toContain( + "are required to complete your command." + ); }); }); }); diff --git a/packages/imperative/src/rest/src/session/ConnectionPropsForSessCfg.ts b/packages/imperative/src/rest/src/session/ConnectionPropsForSessCfg.ts index f7af5e7f4c..4362e8026d 100644 --- a/packages/imperative/src/rest/src/session/ConnectionPropsForSessCfg.ts +++ b/packages/imperative/src/rest/src/session/ConnectionPropsForSessCfg.ts @@ -97,7 +97,7 @@ export class ConnectionPropsForSessCfg { public static async addPropsOrPrompt( initialSessCfg: SessCfgType, cmdArgs: ICommandArguments, - connOpts: IOptionsForAddConnProps = {} + connOpts: IOptionsForAddConnProps = {} ): Promise { const impLogger = Logger.getImperativeLogger(); @@ -113,8 +113,8 @@ export class ConnectionPropsForSessCfg { ); // This function will provide all the needed properties in one array - const promptForValues: (keyof ISession)[] = []; - const doNotPromptForValues: (keyof ISession)[] = []; + let promptForValues: (keyof SessCfgType & string)[] = []; + const doNotPromptForValues: (keyof SessCfgType & string)[] = []; /* Add the override properties to the session object. */ @@ -134,6 +134,17 @@ export class ConnectionPropsForSessCfg { } } + // Set default values on propsToPromptFor + if(connOpts.propsToPromptFor?.length > 0) + { + connOpts.propsToPromptFor.forEach(obj => { + if(obj.secure == null) obj.secure = true; + if(obj.secure) this.secureSessCfgProps.add(obj.name.toString()); + promptForValues.push(obj.name as keyof ISession); + this.promptTextForValues[obj.name.toString()] = obj.description; + }); + } + // check what properties are needed to be prompted if (ConnectionPropsForSessCfg.propHasValue(sessCfgToUse.hostname) === false && !doNotPromptForValues.includes("hostname")) { promptForValues.push("hostname"); @@ -168,6 +179,16 @@ export class ConnectionPropsForSessCfg { // put all the needed properties in an array and call the external function const answers = await connOptsToUse.getValuesBack(promptForValues); + if(connOpts.propsToPromptFor?.length > 0) + { + connOpts.propsToPromptFor.forEach(obj => { + if(obj.isGivenValueValid != null) + { + if(!obj.isGivenValueValid(answers[obj.name])) promptForValues = promptForValues.filter(item => obj.name !== item); + } + }); + } + // validate what values are given back and move it to sessCfgToUse for (const value of promptForValues) { if (ConnectionPropsForSessCfg.propHasValue(answers[value])) { @@ -175,6 +196,7 @@ export class ConnectionPropsForSessCfg { } } + // if (connOptsToUse.autoStore !== false && connOptsToUse.parms != null) { await ConfigAutoStore.storeSessCfgProps(connOptsToUse.parms, sessCfgToUse, promptForValues); } @@ -216,7 +238,7 @@ export class ConnectionPropsForSessCfg { public static resolveSessCfgProps( sessCfg: SessCfgType, cmdArgs: ICommandArguments = { $0: "", _: [] }, - connOpts: IOptionsForAddConnProps = {} + connOpts: IOptionsForAddConnProps = {} ) { const impLogger = Logger.getImperativeLogger(); @@ -307,7 +329,7 @@ export class ConnectionPropsForSessCfg { impLogger.debug("Using basic authentication"); sessCfg.type = SessConstants.AUTH_TYPE_BASIC; } - ConnectionPropsForSessCfg.setTypeForTokenRequest(sessCfg, connOpts, cmdArgs.tokenType); + ConnectionPropsForSessCfg.setTypeForTokenRequest(sessCfg, connOpts, cmdArgs.tokenType); ConnectionPropsForSessCfg.logSessCfg(sessCfg); } @@ -358,7 +380,8 @@ export class ConnectionPropsForSessCfg { * @param connOpts Options for adding connection properties * @returns Name-value pairs of connection properties */ - private static getValuesBack(connOpts: IOptionsForAddConnProps): (properties: string[]) => Promise<{ [key: string]: any }> { + private static getValuesBack(connOpts: IOptionsForAddConnProps): + (properties: string[]) => Promise<{ [key: string]: any }> { return async (promptForValues: string[]) => { /* The check for console.log in the following 'if' statement is only needed for tests * which do not create a mock for the connOpts.parms.response.console.log property. @@ -392,7 +415,7 @@ export class ConnectionPropsForSessCfg { let answer; while (answer === undefined) { const hideText = profileSchema[value]?.secure || this.secureSessCfgProps.has(value); - let promptText = `${this.promptTextForValues[value]} ${serviceDescription}`; + let promptText = `${this.promptTextForValues[value] ?? `Enter your ${value} for`} ${serviceDescription}`; if (hideText) { promptText += " (will be hidden)"; } @@ -446,9 +469,9 @@ export class ConnectionPropsForSessCfg { * @param tokenType * The type of token that we expect to receive. */ - private static setTypeForTokenRequest( - sessCfg: any, - options: IOptionsForAddConnProps, + private static setTypeForTokenRequest( + sessCfg: SessCfgType, + options: IOptionsForAddConnProps, tokenType: SessConstants.TOKEN_TYPE_CHOICES ) { const impLogger = Logger.getImperativeLogger(); diff --git a/packages/imperative/src/rest/src/session/doc/IOptionsForAddConnProps.ts b/packages/imperative/src/rest/src/session/doc/IOptionsForAddConnProps.ts index 706e40a11a..5cece9c5d3 100644 --- a/packages/imperative/src/rest/src/session/doc/IOptionsForAddConnProps.ts +++ b/packages/imperative/src/rest/src/session/doc/IOptionsForAddConnProps.ts @@ -9,18 +9,17 @@ * */ -import { SessConstants } from "../../.."; +import { ISession, SessConstants } from "../../.."; import { IHandlerParameters } from "../../../../cmd"; import { AUTH_TYPE_CHOICES } from "../SessConstants"; import { IOverridePromptConnProps } from "./IOverridePromptConnProps"; - +import { IPropsToPromptFor } from "../doc/IPropsToPromptFor"; /** * Interface for options supplied to ConnectionPropsForSessCfg.addPropsOrPrompt() * @export * @interface ISession */ -export interface IOptionsForAddConnProps { - +export interface IOptionsForAddConnProps { /** * Indicates that we want to generate a token. * When true, we use the user and password for the operation @@ -51,7 +50,13 @@ export interface IOptionsForAddConnProps { * Specifies a list of authentication properties, and what they should override. * If one of these properties is available on the session, do not prompt for the other property. */ - propertyOverrides?: IOverridePromptConnProps[]; + propertyOverrides?: IOverridePromptConnProps[]; + + /** + * Allows passing additional properties for which to prompt. + * Used in cases of an incorrect or missing key passphrase. + */ + propsToPromptFor?: IPropsToPromptFor[]; /** * Specifies the functionality that external applications will use for prompting. diff --git a/packages/imperative/src/rest/src/session/doc/IOverridePromptConnProps.ts b/packages/imperative/src/rest/src/session/doc/IOverridePromptConnProps.ts index db0a928409..868ba5e32b 100644 --- a/packages/imperative/src/rest/src/session/doc/IOverridePromptConnProps.ts +++ b/packages/imperative/src/rest/src/session/doc/IOverridePromptConnProps.ts @@ -16,7 +16,7 @@ import { ISession } from "./ISession"; * @export * @interface IOverridePromptConnProps */ -export interface IOverridePromptConnProps { +export interface IOverridePromptConnProps { /** * Indicates the session property that should be considered in the prompting logic. */ @@ -34,5 +34,11 @@ export interface IOverridePromptConnProps { * Prompting logic is only in place for host, port, user, and password, but cert, certKey, tokenType, and tokenValue may also need * to be overridden. */ - propertiesOverridden: (keyof ISession)[]; + propertiesOverridden: (keyof SessCfgType & string)[]; + + /** + * Allows passing additional properties for which to prompt. + * Used in cases of an incorrect or missing key passphrase. + */ + propsToPromptFor?: (keyof SessCfgType & string)[]; } \ No newline at end of file diff --git a/packages/imperative/src/rest/src/session/doc/IPropsToPromptFor.ts b/packages/imperative/src/rest/src/session/doc/IPropsToPromptFor.ts new file mode 100644 index 0000000000..5cc72b86b3 --- /dev/null +++ b/packages/imperative/src/rest/src/session/doc/IPropsToPromptFor.ts @@ -0,0 +1,19 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +import { ISession } from './ISession'; + +export interface IPropsToPromptFor { + name: keyof SessCfgType & string, + secure?: boolean, + description?: string, + isGivenValueValid?: (givenValue: string) => boolean +} \ No newline at end of file diff --git a/packages/zosuss/CHANGELOG.md b/packages/zosuss/CHANGELOG.md index 1bce6dca38..3828fcd3fb 100644 --- a/packages/zosuss/CHANGELOG.md +++ b/packages/zosuss/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to the Zowe z/OS USS SDK 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) +- Enhancement: `SshBaseHandler` command processor will now prompt user up to 3 times to enter the correct keyPassphrase in the case that the stored value is incorrect or no value is stored. [#1770](https://github.com/zowe/zowe-cli/issues/1770) + ## `8.0.0-next.202403132009` - Enhancement: Provide more legible errors to user when they are missing user/password credentials while diff --git a/packages/zosuss/__tests__/__unit__/SshBaseHandler.unit.test.ts b/packages/zosuss/__tests__/__unit__/SshBaseHandler.unit.test.ts new file mode 100644 index 0000000000..5e9365250a --- /dev/null +++ b/packages/zosuss/__tests__/__unit__/SshBaseHandler.unit.test.ts @@ -0,0 +1,231 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +import { IHandlerParameters, IProfile, CommandProfiles, ConnectionPropsForSessCfg } from "@zowe/imperative"; +import { mockHandlerParameters } from "@zowe/cli-test-utils"; +import { join, normalize } from "path"; +import { Shell } from "../../src/Shell"; +import { SshBaseHandler } from "../../src/SshBaseHandler"; +import * as fs from "fs"; + +process.env.FORCE_COLOR = "0"; + +const UNIT_TEST_SSH_PROF_OPTS = { + host: "somewhere.com", + port: "22", + user: "someone", + password: "somesecret" +}; + +const UNIT_TEST_SSH_PROF_OPTS_PRIVATE_KEY = { + host: "somewhere.com", + port: "22", + 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(); +UNIT_TEST_PROFILE_MAP.set( + "ssh", [{ + name: "ssh", + type: "ssh", + ...UNIT_TEST_SSH_PROF_OPTS + }] +); +const UNIT_TEST_PROFILES_SSH = new CommandProfiles(UNIT_TEST_PROFILE_MAP); + +const UNIT_TEST_PROFILE_MAP_PRIVATE_KEY = new Map(); +UNIT_TEST_PROFILE_MAP_PRIVATE_KEY.set( + "ssh", [{ + name: "ssh", + type: "ssh", + ...UNIT_TEST_SSH_PROF_OPTS_PRIVATE_KEY + }] +); +const UNIT_TEST_PROFILE_MAP_PRIVATE_KEY_WITH_PASSPHRASE = new Map(); +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(); +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({ + arguments: UNIT_TEST_SSH_PROF_OPTS, + positionals: ["zos-uss", "issue", "ssh"], + definition: {} as any, + profiles: UNIT_TEST_PROFILES_SSH +}); + +const DEFAULT_PARAMETERS_PRIVATE_KEY: IHandlerParameters = mockHandlerParameters({ + arguments: UNIT_TEST_SSH_PROF_OPTS_PRIVATE_KEY, + positionals: ["zos-uss", "issue", "ssh"], + definition: {} as any, + 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: {} as any, + 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: {} as any, + profiles: UNIT_TEST_PROFILES_SSH_PRIVATE_KEY_WITH_PASSPHRASE_NO_USER, +}); + +class myHandler extends SshBaseHandler { + public async processCmd(commandParameters: IHandlerParameters): Promise { + return await Shell.executeSsh( + this.mSession, + commandParameters.arguments.command, + (data: any) => commandParameters.response.console.log(Buffer.from(data)) + ); + } +} +const testOutput = "TEST OUTPUT"; + +describe("issue ssh handler tests", () => { + + afterEach(() => { + jest.resetAllMocks(); + }); + + it("should be able to get stdout", async () => { + Shell.executeSsh = jest.fn(async (session, command, stdoutHandler) => { + stdoutHandler(testOutput); + }); + const handler = new myHandler(); + 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 myHandler(); + 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); + }); + jest.spyOn(fs,"readFileSync").mockReturnValue("dummyPrivateKey"); + const handler = new myHandler(); + jest.spyOn(handler,"processCmd").mockImplementationOnce(() => {throw new Error("but no passphrase given");}); + const params = Object.assign({}, ...[DEFAULT_PARAMETERS_KEY_PASSPHRASE]); + params.arguments.command = "echo test"; + 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); + }); + jest.spyOn(fs,"readFileSync").mockReturnValue("dummyPrivateKey"); + const handler = new myHandler(); + 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 myHandler(); + 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 myHandler(); + 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 myHandler(); + const params = Object.assign({}, ...[DEFAULT_PARAMETERS_PRIVATE_KEY]); + params.arguments.command = "pwd"; + await handler.process(params); + expect(Shell.executeSsh).toHaveBeenCalledTimes(1); + expect(testOutput).toMatchSnapshot(); + }); +}); diff --git a/packages/zosuss/__tests__/__unit__/__snapshots__/SshBaseHandler.unit.test.ts.snap b/packages/zosuss/__tests__/__unit__/__snapshots__/SshBaseHandler.unit.test.ts.snap new file mode 100644 index 0000000000..45544acebb --- /dev/null +++ b/packages/zosuss/__tests__/__unit__/__snapshots__/SshBaseHandler.unit.test.ts.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`issue ssh handler tests should be able to get stdout 1`] = `"TEST OUTPUT"`; + +exports[`issue ssh handler tests should be able to get stdout 2`] = `"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"`; diff --git a/packages/zosuss/src/SshBaseHandler.ts b/packages/zosuss/src/SshBaseHandler.ts index cf76092a5f..d3927573e4 100644 --- a/packages/zosuss/src/SshBaseHandler.ts +++ b/packages/zosuss/src/SshBaseHandler.ts @@ -22,16 +22,17 @@ import { IImperativeError, ImperativeError, ConnectionPropsForSessCfg, - SessConstants + SessConstants, } from "@zowe/imperative"; import { SshSession } from "./SshSession"; import { ISshSession } from "./doc/ISshSession"; +import { utils } from "ssh2"; +import * as fs from "fs"; /** * This class is used by the various handlers in the project as the base class for their implementation. */ export abstract class SshBaseHandler implements ICommandHandler { - /** * The session creating from the command line arguments / profile */ @@ -58,22 +59,107 @@ export abstract class SshBaseHandler implements ICommandHandler { public async process(commandParameters: IHandlerParameters) { this.mHandlerParams = commandParameters; - const sshSessCfgOverride: IOverridePromptConnProps[] = [{ - propertyName: "privateKey", - propertiesOverridden: ["password", "tokenType", "tokenValue", "cert", "certKey", "passphrase"] - }]; - const sshSessCfg: ISshSession = SshSession.createSshSessCfgFromArgs(commandParameters.arguments); - const sshSessCfgWithCreds = await ConnectionPropsForSessCfg.addPropsOrPrompt( - sshSessCfg, commandParameters.arguments, { - parms: commandParameters, - propertyOverrides: sshSessCfgOverride, - supportedAuthTypes: [SessConstants.AUTH_TYPE_BASIC] - } + const sshSessCfgOverride: IOverridePromptConnProps[] = [ + { + propertyName: "privateKey", + propertiesOverridden: [ + "password", + "tokenType", + "tokenValue", + "cert", + "certKey", + ], + }, + ]; + const sshSessCfg: ISshSession = SshSession.createSshSessCfgFromArgs( + commandParameters.arguments ); + let sshSessCfgWithCreds = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + sshSessCfg, + commandParameters.arguments, + { + parms: commandParameters, + propertyOverrides: sshSessCfgOverride, + supportedAuthTypes: [SessConstants.AUTH_TYPE_BASIC], + } + ); this.mSession = new SshSession(sshSessCfgWithCreds); this.mArguments = commandParameters.arguments; - await this.processCmd(commandParameters); + + try { + await this.processCmd(commandParameters); + } catch (e) { + if ( + e.message.includes("but no passphrase given") || + e.message.includes("bad passphrase?") + ) { + this.console.log("Initial key passphrase authentication failed!" + "\n"); + const maxAttempts = 3; + let attempt = 0; + let success = false; + while (attempt < maxAttempts && !success) { + try { + sshSessCfgWithCreds = + await ConnectionPropsForSessCfg.addPropsOrPrompt( + sshSessCfgWithCreds, + commandParameters.arguments, + { + parms: commandParameters, + propertyOverrides: sshSessCfgOverride, + supportedAuthTypes: [ + SessConstants.AUTH_TYPE_BASIC, + ], + propsToPromptFor: [ + { + name: "keyPassphrase", + isGivenValueValid: (givenValue: string) => { + let saveKP: boolean = true; + const result = utils.parseKey( + fs.readFileSync( + sshSessCfgWithCreds.privateKey + ), + givenValue + ); + if (result instanceof Error) + saveKP = + !result.message.includes( + "no passphrase given" + ) && + !result.message.includes( + "bad passphrase" + ); + return saveKP; + }, + }, + ], + } + ); + this.mSession = new SshSession(sshSessCfgWithCreds); + await this.processCmd(commandParameters); + success = true; + } catch (retryError) { + this.console.log( + "\n" + + `Key passphrase authentication failed! (${ + ++attempt + }/${maxAttempts})` + + "\n" + ); + if (attempt >= maxAttempts) { + throw new Error( + "Maximum retry attempts reached. Authentication failed." + ); + } + } + } + } + else + { + throw e; + } + } } /** @@ -122,5 +208,7 @@ export abstract class SshBaseHandler implements ICommandHandler { * @param {IHandlerParameters} commandParameters Command parameters sent to the handler. * */ - public abstract processCmd(commandParameters: IHandlerParameters): Promise; + public abstract processCmd( + commandParameters: IHandlerParameters + ): Promise; }