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

[v3] Config profile set #2265

Merged
merged 16 commits into from
Oct 1, 2024
5 changes: 5 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

- Enhancement: Added the ability to specify a profile with the `zowe config secure` command. This allows the user to prompt for the secure values of the specified profile. [#1890] https://github.com/zowe/zowe-cli/issues/1890

## `8.0.1`

- BugFix: Removed Secrets SDK requirement when Imperative is a bundled dependency. [#2276](https://github.com/zowe/zowe-cli/issues/2276)
Expand All @@ -15,6 +19,7 @@ All notable changes to the Imperative package will be documented in this file.
- Update: Final prerelease
- Update: See `5.27.1` for details


## `8.0.0-next.202408301809`

- LTS Breaking: Removed the following obsolete V1 profile classes/functions:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*/

import { Logger } from "../../../../../logger";
import { Config } from "../../../../../config/src/Config";
import { Config } from "../../../../../config";
import { IConfig, IConfigOpts, IConfigProfile } from "../../../../../config";
import { ImperativeConfig } from "../../../../../utilities";
import { IImperativeConfig } from "../../../../src/doc/IImperativeConfig";
Expand Down Expand Up @@ -760,7 +760,7 @@ describe("Configuration Secure command handler", () => {
expect(writeFileSyncSpy).toHaveBeenNthCalledWith(1, fakeProjPath, JSON.stringify(compObj, null, 4)); // Config
});

it("should fail to invoke auth handler if it throws an error", async () => {
it("should only prompt for profiles that matched profile param", async () => {
const eco = lodash.cloneDeep(expectedProjConfigObjectWithToken);

readFileSyncSpy.mockReturnValueOnce(JSON.stringify(eco));
Expand Down Expand Up @@ -792,5 +792,112 @@ describe("Configuration Secure command handler", () => {
expect(keytarSetPasswordSpy).toHaveBeenCalledTimes(0);
expect(writeFileSyncSpy).toHaveBeenCalledTimes(0);
});
describe("profile param tests", () => {
let handler: SecureHandler;
let params: any;
let myPromptSpy: jest.SpyInstance;

beforeEach(() => {
handler = new SecureHandler();
params = getIHandlerParametersObject();

params.arguments.userConfig = true;
params.arguments.globalConfig = true;

// Mock the console prompt to return an empty string
myPromptSpy = jest.spyOn(params.response.console, "prompt").mockResolvedValue("");

// Reset spies
keytarGetPasswordSpy.mockReturnValue(fakeSecureData);
keytarSetPasswordSpy.mockImplementation();
keytarDeletePasswordSpy.mockImplementation();
readFileSyncSpy = jest.spyOn(fs, "readFileSync");
writeFileSyncSpy = jest.spyOn(fs, "writeFileSync");
existsSyncSpy = jest.spyOn(fs, "existsSync");
writeFileSyncSpy.mockImplementation();
});

const runTest = async (profile: string, secureFields: string[], expectedPromptTimes: number, expectedSecureField: string) => {
params.arguments.profile = profile;

// Mock fs calls
const eco = lodash.cloneDeep(expectedGlobalUserConfigObject);
eco.$schema = "./fakeapp.schema.json";
readFileSyncSpy.mockReturnValueOnce(JSON.stringify(eco));
existsSyncSpy.mockReturnValueOnce(false).mockReturnValueOnce(false).mockReturnValueOnce(true).mockReturnValue(false);
searchSpy.mockReturnValueOnce(fakeProjUserPath).mockReturnValueOnce(fakeProjPath);
await setupConfigToLoad(undefined, configOpts);

// Setup mock secure fields
jest.spyOn(ImperativeConfig.instance.config.api.secure, "secureFields").mockReturnValue(secureFields);

let caughtError;
try {
await handler.process(params);
} catch (error) {
caughtError = error;
}

// Verify prompt count and inclusion of expected secure fields
expect(myPromptSpy).toHaveBeenCalledTimes(expectedPromptTimes);
if (expectedPromptTimes > 0) {
expect(myPromptSpy).toHaveBeenCalledWith(expect.stringContaining(expectedSecureField), { "hideText": true });
}
expect(caughtError).toBeUndefined();
};

it("should only prompt for secure values that match the profile passed in through params", async () => {
await runTest(
"GoodProfile",
[
"profiles.noMatchProfile.properties.tokenValue",
"profiles.GoodProfile.properties.tokenValue",
"profiles.abcdefg.properties.tokenValue"
],
1,
"profiles.GoodProfile.properties.tokenValue"
);
});

it("should only prompt for secure values that match the profile passed in through params - nested profile", async () => {
await runTest(
"lpar1.GoodProfile",
[
"profiles.noMatchProfile.properties.tokenValue",
"profiles.lpar1.profiles.GoodProfile.properties.tokenValue",
"profiles.abcdefg.properties.tokenValue"
],
1,
"profiles.lpar1.profiles.GoodProfile.properties.tokenValue"
);
});

it("should only prompt for secure values that match the profile passed in through params - ignore casing", async () => {
await runTest(
"gOODpROFILE",
[
"profiles.noMatchProfile.properties.tokenValue",
"profiles.GoodProfile.properties.tokenValue",
"profiles.abcdefg.properties.tokenValue"
],
1,
"profiles.GoodProfile.properties.tokenValue"
);
});

it("should prompt for all secure values given a profile in which no secure profile value matches", async () => {
await runTest(
"noMatchProfile",
[
"profiles.lpar1.profiles.test.properties.tokenValue",
"profiles.GoodProfile.properties.tokenValue",
"profiles.abcdefg.properties.tokenValue"
],
3,
"profiles.lpar1.profiles.test.properties.tokenValue"
);
});
});

});
});
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ export const secureDefinition: ICommandDefinition = {
aliases: ["p"],
type: "boolean",
defaultValue: false
},
{
name: "profile",
description: "Specify the profile for which you want to configure secure values.",
type: "string",
aliases: ["pf"],
defaultValue: null
}
],
examples: [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,36 @@
/*
* 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.
*
*/
* 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 { ICommandArguments, ICommandHandler, IHandlerParameters } from "../../../../../cmd";
import {
ICommandArguments,
ICommandHandler,
IHandlerParameters,
} from "../../../../../cmd";
import { Config, ConfigConstants, ConfigSchema } from "../../../../../config";
import { ConfigAutoStore } from "../../../../../config/src/ConfigAutoStore";
import { ConfigUtils } from "../../../../../config/src/ConfigUtils";
import { ImperativeError } from "../../../../../error";
import { Logger } from "../../../../../logger";
import { ConnectionPropsForSessCfg, ISession, Session } from "../../../../../rest";
import {
ConnectionPropsForSessCfg,
ISession,
Session,
} from "../../../../../rest";
import { ImperativeConfig } from "../../../../../utilities";

export default class SecureHandler implements ICommandHandler {
/**
* The parameters object passed to the command handler.
*/
private params: IHandlerParameters;

/**
* Process the command and input.
*
Expand All @@ -44,38 +51,81 @@
const prunedFiles = config.api.secure.rmUnusedProps();
if (prunedFiles.length > 0) {
await config.api.secure.directSave();
params.response.console.log("Deleted secure properties for the following missing files:\n\t" + prunedFiles.join("\n\t") + "\n");
params.response.console.log(
"Deleted secure properties for the following missing files:\n\t" +
prunedFiles.join("\n\t") +
"\n"
);
}
}

// Create the config, load the secure values, and activate the desired layer
config.api.layers.activate(params.arguments.userConfig, params.arguments.globalConfig);
const secureProps: string[] = config.api.secure.secureFields();
config.api.layers.activate(
params.arguments.userConfig,
params.arguments.globalConfig
);
let secureProps: string[] = config.api.secure.secureFields();

if (secureProps.length === 0) {
params.response.console.log("No secure properties found in your config");
params.response.console.log(
"No secure properties found in your config"
);
return;
}

if (params.arguments.profile) {
const filteredSecureProps = secureProps.filter(
(prop) =>
config.api.profiles
.getProfileNameFromPath(prop)
.toLowerCase() ===
params.arguments.profile.toLowerCase()
);
if (filteredSecureProps.length === 0 && secureProps.length > 0) {
params.response.console.log(
`No secure properties from profile '${params.arguments.profile}' found.`
);
} else {
secureProps = filteredSecureProps;
}
}
// Prompt for values designated as secure
for (const propName of secureProps) {
if (propName.endsWith(".tokenValue")) {
params.response.console.log(`Processing secure properties for profile: ${config.api.profiles.getProfileNameFromPath(propName)}`);
let propValue = await this.handlePromptForAuthToken(config, propName);
params.response.console.log(
`Processing secure properties for profile: ${config.api.profiles.getProfileNameFromPath(
propName
)}`
);
let propValue = await this.handlePromptForAuthToken(
config,
propName
);
if (propValue === undefined) {
propValue = await params.response.console.prompt(`Enter ${propName} ${ConfigConstants.SKIP_PROMPT}`, {hideText: true});
propValue = await params.response.console.prompt(
`Enter ${propName} ${ConfigConstants.SKIP_PROMPT}`,
{ hideText: true }
);
}

// Save the value in the config securely
if (propValue) {
config.set(propName, propValue, { secure: true });
}
} else {
let propValue = await params.response.console.prompt(`Enter ${propName} ${ConfigConstants.SKIP_PROMPT}`, { hideText: true });
let propValue = await params.response.console.prompt(
`Enter ${propName} ${ConfigConstants.SKIP_PROMPT}`,
{ hideText: true }
);

// Save the value in the config securely
if (propValue) {
propValue = ConfigUtils.coercePropValue(propValue, ConfigSchema.findPropertyType(propName, config.properties));
propValue = ConfigUtils.coercePropValue(
propValue,
ConfigSchema.findPropertyType(
propName,
config.properties
)
);
config.set(propName, propValue, { secure: true });
}
}
Expand All @@ -93,31 +143,57 @@
* @param propPath JSON property path of the auth token
* @returns Token value, or undefined if none was obtained
*/
private async handlePromptForAuthToken(config: Config, propPath: string): Promise<string | undefined> {
private async handlePromptForAuthToken(
config: Config,
propPath: string
): Promise<string | undefined> {
const profilePath = propPath.slice(0, propPath.indexOf(".properties"));
const authHandlerClass = ConfigAutoStore.findAuthHandlerForProfile(profilePath, this.params.arguments);
const authHandlerClass = ConfigAutoStore.findAuthHandlerForProfile(
profilePath,
this.params.arguments
);

if (authHandlerClass != null) {
const api = authHandlerClass.getAuthHandlerApi();
if (api.promptParams.serviceDescription != null) {
this.params.response.console.log(`Logging in to ${api.promptParams.serviceDescription} ${ConfigConstants.SKIP_PROMPT}`);
this.params.response.console.log(

Check warning on line 159 in packages/imperative/src/imperative/src/config/cmd/secure/secure.handler.ts

View check run for this annotation

Codecov / codecov/patch

packages/imperative/src/imperative/src/config/cmd/secure/secure.handler.ts#L159

Added line #L159 was not covered by tests
`Logging in to ${api.promptParams.serviceDescription} ${ConfigConstants.SKIP_PROMPT}`
);
}

const profile = config.api.profiles.get(profilePath.replace(/profiles\./g, ""), false);
const sessCfg: ISession = api.createSessCfg(profile as ICommandArguments);
const sessCfgWithCreds = await ConnectionPropsForSessCfg.addPropsOrPrompt(sessCfg, profile as ICommandArguments,
{ parms: this.params, doPrompting: true, requestToken: true, ...api.promptParams });
Logger.getAppLogger().info(`Fetching ${profile.tokenType} for ${propPath}`);
const profile = config.api.profiles.get(
profilePath.replace(/profiles\./g, ""),
false
);
const sessCfg: ISession = api.createSessCfg(
profile as ICommandArguments
);
const sessCfgWithCreds =
await ConnectionPropsForSessCfg.addPropsOrPrompt(
sessCfg,
profile as ICommandArguments,
{
parms: this.params,
doPrompting: true,
requestToken: true,
...api.promptParams,
}
);
Logger.getAppLogger().info(
`Fetching ${profile.tokenType} for ${propPath}`
);

if (ConnectionPropsForSessCfg.sessHasCreds(sessCfgWithCreds)) {
try {
const tokenValue = await api.sessionLogin(new Session(sessCfgWithCreds));
const tokenValue = await api.sessionLogin(
new Session(sessCfgWithCreds)
);
this.params.response.console.log("Logged in successfully");
return tokenValue;
} catch (error) {
throw new ImperativeError({
msg: `Failed to fetch ${profile.tokenType} for ${propPath}: ${error.message}`,
causeErrors: error
causeErrors: error,
});
}
} else {
Expand Down