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

Fix Config API bugs that affect extenders converting V1 profiles #2313

Merged
merged 12 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/imperative/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

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

## Recent Changes

- BugFix: Fixed an issue where the `ProfileInfo.profileManagerWillLoad` method failed if profiles were not yet read from disk. [#2284](https://github.com/zowe/zowe-cli/issues/2284)
- BugFix: Fixed an issue where the `ProfileInfo.onlyV1ProfilesExist` method could wrongly return true when V2 profiles exist. [#2311](https://github.com/zowe/zowe-cli/issues/2311)
t1m0thyj marked this conversation as resolved.
Show resolved Hide resolved
- Deprecated the static method `ProfileInfo.onlyV1ProfilesExist` and replaced it with an `onlyV1ProfilesExist` instance method on the `ProfileInfo` class.
- BugFix: Fixed an issue where the `ConvertV1Profiles.convert` method may create team configuration files in the wrong directory if the environment variable `ZOWE_CLI_HOME` is set. [#2312](https://github.com/zowe/zowe-cli/issues/2312)
- BugFix: Fixed an issue where the Imperative Event Emitter would fire event callbacks for the same app that triggered the event. [#2279](https://github.com/zowe/zowe-cli/issues/2279)
- BugFix: Fixed an issue where the `ProfileInfo.updateKnownProperty` method could rewrite team config file to disk without any changes when storing secure value. [#2324](https://github.com/zowe/zowe-cli/issues/2324)

## `8.8.0`

- BugFix: Enabled commands with either the `--help` or `--version` flags to still display their information despite any configuration file issues. [#2301](https://github.com/zowe/zowe-cli/pull/2301)
Expand Down
28 changes: 24 additions & 4 deletions packages/imperative/src/config/__tests__/ConfigUtils.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import * as fs from "fs";
import * as path from "path";
import * as os from "os";
import * as jsonfile from "jsonfile";
import * as glob from "fast-glob";
import { ConfigUtils } from "../../config/src/ConfigUtils";
import { CredentialManagerFactory } from "../../security";
import { ImperativeConfig } from "../../utilities";
Expand Down Expand Up @@ -132,10 +133,29 @@ describe("Config Utils", () => {
})
} as any);

const fsExistsSyncSpy = jest.spyOn(fs, "existsSync").mockReturnValueOnce(false);
const globSyncSpy = jest.spyOn(glob, "sync").mockReturnValueOnce([]);

expect(ConfigUtils.onlyV1ProfilesExist).toBe(false);
expect(fsExistsSyncSpy).toHaveBeenCalledTimes(1);
expect(globSyncSpy).toHaveBeenCalledTimes(1);
});

it("should return false when only V1 profile meta files exist", () => {
jest.spyOn(ImperativeConfig, "instance", "get").mockReturnValue({
config: {
exists: false
},
cliHome: "/fake/cli/home/dir",
loadedConfig: jest.fn(() => {
return {
envVariablePrefix: "Fake_cli_prefix"
};
})
} as any);

const globSyncSpy = jest.spyOn(glob, "sync").mockReturnValueOnce(["profiles/zosmf/zosmf_meta.yaml"]);

expect(ConfigUtils.onlyV1ProfilesExist).toBe(false);
expect(globSyncSpy).toHaveBeenCalledTimes(1);
});

it("should return true when only V1 profiles exist", () => {
Expand All @@ -151,10 +171,10 @@ describe("Config Utils", () => {
})
} as any);

const fsExistsSyncSpy = jest.spyOn(fs, "existsSync").mockReturnValueOnce(true);
const globSyncSpy = jest.spyOn(glob, "sync").mockReturnValueOnce(["profiles/zosmf/lpar1.yaml"]);

expect(ConfigUtils.onlyV1ProfilesExist).toBe(true);
expect(fsExistsSyncSpy).toHaveBeenCalledTimes(1);
expect(globSyncSpy).toHaveBeenCalledTimes(1);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jest.mock("jsonfile");
import * as fs from "fs";
import * as jsonfile from "jsonfile";
import { CredentialManagerFactory } from "../..";
import { ConvertV1Profiles } from "../";
import { Config, ConvertV1Profiles } from "../";
import { ConvertMsgFmt } from "../src/doc/IConvertV1Profiles";
import { ImperativeConfig } from "../../utilities/src/ImperativeConfig";
import { ImperativeError } from "../../error/src/ImperativeError";
Expand Down Expand Up @@ -425,6 +425,18 @@ describe("ConvertV1Profiles tests", () => {
expect(getOldProfileCountSpy).toHaveBeenCalled();
expect(convNeeded).toEqual(true);
});

it("should create a client config instance if it does not exist yet", async () => {
delete (ImperativeConfig.instance as any).config;
const configLoadSpy = jest.spyOn(Config, "load").mockResolvedValueOnce({ exists: true } as any);

// call the function that we want to test
// using class["name"] notation because it is a private static function
const convNeeded = await ConvertV1Profiles["isConversionNeeded"]();

expect(configLoadSpy).toHaveBeenCalledWith("zowe", { homeDir: ImperativeConfig.instance.cliHome });
expect(convNeeded).toEqual(false);
});
}); // end isConversionNeeded

describe("moveV1ProfilesToConfigFile", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,16 +137,25 @@ describe("TeamConfig ProfileInfo tests", () => {
expect(profLoaded.profile.profLoc.jsonLoc).toBe(profAttrs.profLoc.jsonLoc);
expect(profLoaded.profile.isDefaultProfile).toBe(profAttrs.isDefaultProfile);
});
});

describe("onlyV1ProfilesExist", () => {
it("should detect that V2 profiles exist", async () => {
const profInfo = createNewProfInfo(teamProjDir);
const v2ExistsSpy = jest.spyOn(profInfo, "getTeamConfig").mockReturnValue({ exists: true } as any);
const v1ExistsSpy = jest.spyOn(ConfigUtils, "onlyV1ProfilesExist", "get");
expect(profInfo.onlyV1ProfilesExist).toBe(false);
expect(v2ExistsSpy).toHaveBeenCalledTimes(1);
expect(v1ExistsSpy).not.toHaveBeenCalled();
});

it("should detect that only V1 profiles exist", async () => {
// onlyV1ProfilesExist is a getter of a property, so mock the property
Object.defineProperty(ConfigUtils, "onlyV1ProfilesExist", {
configurable: true,
get: jest.fn(() => {
return true;
})
});
expect(ProfileInfo.onlyV1ProfilesExist).toBe(true);
const profInfo = createNewProfInfo(teamProjDir);
const v2ExistsSpy = jest.spyOn(profInfo, "getTeamConfig").mockReturnValue({ exists: false } as any);
const v1ExistsSpy = jest.spyOn(ConfigUtils, "onlyV1ProfilesExist", "get").mockReturnValueOnce(true);
expect(profInfo.onlyV1ProfilesExist).toBe(true);
expect(v2ExistsSpy).toHaveBeenCalledTimes(1);
expect(v1ExistsSpy).toHaveBeenCalledTimes(1);
});
});

Expand Down Expand Up @@ -311,8 +320,8 @@ describe("TeamConfig ProfileInfo tests", () => {
describe("profileManagerWillLoad", () => {
it("should return false if secure credentials fail to load", async () => {
const profInfo = createNewProfInfo(teamProjDir);
jest.spyOn((profInfo as any).mCredentials, "isSecured", "get").mockReturnValueOnce(true);
jest.spyOn((profInfo as any).mCredentials, "loadManager").mockImplementationOnce(async () => {
jest.spyOn((profInfo as any).mCredentials, "isCredentialManagerInAppSettings").mockReturnValueOnce(true);
jest.spyOn((profInfo as any).mCredentials, "activateCredMgrOverride").mockImplementationOnce(async () => {
throw new Error("bad credential manager");
});

Expand All @@ -327,10 +336,10 @@ describe("TeamConfig ProfileInfo tests", () => {
expect(response).toEqual(true);
});

it("should return true if credentials are not secure", async () => {
it("should return true if there is no credential manager", async () => {
// ensure that we are not in the team project directory
const profInfo = createNewProfInfo(origDir);
(profInfo as any).mCredentials = { isSecured: false };
jest.spyOn((profInfo as any).mCredentials, "isCredentialManagerInAppSettings").mockReturnValueOnce(false);
const response = await profInfo.profileManagerWillLoad();
expect(response).toEqual(true);
});
Expand Down Expand Up @@ -1297,7 +1306,7 @@ describe("TeamConfig ProfileInfo tests", () => {
});

describe("updateKnownProperty", () => {
it("should throw and error if the property location type is invalid", async () => {
it("should throw an error if the property location type is invalid", async () => {
const profInfo = createNewProfInfo(teamProjDir);
await profInfo.readProfilesFromDisk();
let caughtError;
Expand Down Expand Up @@ -1336,6 +1345,7 @@ describe("TeamConfig ProfileInfo tests", () => {
const profInfo = createNewProfInfo(teamProjDir);
await profInfo.readProfilesFromDisk();
const jsonPathMatchesSpy = jest.spyOn(ConfigUtils, "jsonPathMatches");
const configSaveSpy = jest.spyOn(profInfo.getTeamConfig(), "save");

const prof = profInfo.mergeArgsForProfile(profInfo.getAllProfiles("dummy")[0]);
const ret = await profInfo.updateKnownProperty({ mergedArgs: prof, property: "host", value: "example.com" });
Expand All @@ -1344,6 +1354,26 @@ describe("TeamConfig ProfileInfo tests", () => {
expect(newHost).toEqual("example.com");
expect(ret).toBe(true);
expect(jsonPathMatchesSpy).toHaveBeenCalled(); // Verify that profile names are matched correctly
expect(configSaveSpy).toHaveBeenCalled();
});

it("should update the given property in the vault and return true", async () => {
const profInfo = createNewProfInfo(teamProjDir);
await profInfo.readProfilesFromDisk();
const jsonPathMatchesSpy = jest.spyOn(ConfigUtils, "jsonPathMatches");
jest.spyOn(profInfo.getTeamConfig().api.secure, "secureFields").mockReturnValue(["profiles.LPAR4.properties.host"]);
const configSaveSpy = jest.spyOn(profInfo.getTeamConfig(), "save");
const configSecureSaveSpy = jest.spyOn(profInfo.getTeamConfig().api.secure, "save");

const prof = profInfo.mergeArgsForProfile(profInfo.getAllProfiles("dummy")[0]);
const ret = await profInfo.updateKnownProperty({ mergedArgs: prof, property: "host", value: "example.com", setSecure: true });
const newHost = profInfo.getTeamConfig().api.layers.get().properties.profiles.LPAR4.properties.host;

expect(newHost).toEqual("example.com");
expect(ret).toBe(true);
expect(jsonPathMatchesSpy).toHaveBeenCalled(); // Verify that profile names are matched correctly
expect(configSaveSpy).not.toHaveBeenCalled();
expect(configSecureSaveSpy).toHaveBeenCalled();
});

it("should remove the given property if the value specified if undefined", async () => {
Expand Down
46 changes: 21 additions & 25 deletions packages/imperative/src/config/src/ConfigUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
*
*/

import { homedir as osHomedir } from "os";
import { normalize as pathNormalize, join as pathJoin } from "path";
import { existsSync as fsExistsSync } from "fs";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import * as glob from "fast-glob";
t1m0thyj marked this conversation as resolved.
Show resolved Hide resolved
import * as jsonfile from "jsonfile";

import { CredentialManagerFactory } from "../../security/src/CredentialManagerFactory";
Expand All @@ -32,7 +33,7 @@ export class ConfigUtils {
* @returns {string} - Returns the Zowe home directory
*/
public static getZoweDir(): string {
const defaultHome = pathJoin(osHomedir(), ".zowe");
const defaultHome = path.join(os.homedir(), ".zowe");
if (ImperativeConfig.instance.loadedConfig?.defaultHome !== defaultHome) {
ImperativeConfig.instance.loadedConfig = {
name: "zowe",
Expand All @@ -52,8 +53,8 @@ export class ConfigUtils {
*/
public static readExtendersJson(): IExtendersJsonOpts {
const cliHome = ImperativeConfig.instance.loadedConfig != null ? ImperativeConfig.instance.cliHome : ConfigUtils.getZoweDir();
const extenderJsonPath = pathJoin(cliHome, "extenders.json");
if (!fsExistsSync(extenderJsonPath)) {
const extenderJsonPath = path.join(cliHome, "extenders.json");
if (!fs.existsSync(extenderJsonPath)) {
jsonfile.writeFileSync(extenderJsonPath, {
profileTypes: {}
}, { spaces: 4 });
Expand All @@ -70,7 +71,7 @@ export class ConfigUtils {
*/
public static writeExtendersJson(obj: IExtendersJsonOpts): boolean {
try {
const extenderJsonPath = pathJoin(ConfigUtils.getZoweDir(), "extenders.json");
const extenderJsonPath = path.join(ConfigUtils.getZoweDir(), "extenders.json");
jsonfile.writeFileSync(extenderJsonPath, obj, { spaces: 4 });
} catch (err) {
return false;
Expand Down Expand Up @@ -144,20 +145,14 @@ export class ConfigUtils {
return false;
}

let v1ZosmfProfileFileNm: string;
try {
v1ZosmfProfileFileNm = pathNormalize(ImperativeConfig.instance.cliHome + "/profiles/zosmf/zosmf_meta.yaml");
} catch (_thrownErr) {
// We failed to get the CLI home directory. So, we definitely have no V1 profiles.
return false;
}

if (fsExistsSync(v1ZosmfProfileFileNm)) {
// we found V1 profiles
return true;
}

return false;
// look for V1 profiles in the CLI home directory
const v1ProfilePaths = glob.sync("profiles/**/*.yaml", { cwd: ImperativeConfig.instance.cliHome })
.filter(filename => {
// ignore meta yaml files
const { dir, name } = path.parse(filename);
return name !== path.basename(dir) + "_meta";
});
return v1ProfilePaths.length > 0;
}

/**
Expand Down Expand Up @@ -190,10 +185,10 @@ export class ConfigUtils {
const envVarNm = envVarPrefix + EnvironmentalVariableSettings.CLI_HOME_SUFFIX;
if (process.env[envVarNm] === undefined) {
// use OS home directory
homeDir = pathJoin(osHomedir(), "." + appName.toLowerCase());
homeDir = path.join(os.homedir(), "." + appName.toLowerCase());
} else {
// use the available environment variable
homeDir = pathNormalize(process.env[envVarNm]);
homeDir = path.normalize(process.env[envVarNm]);
}
ImperativeConfig.instance.loadedConfig = {
name: appName,
Expand Down Expand Up @@ -300,8 +295,9 @@ export class ConfigUtils {
* Checks if the given token has expired. Supports JSON web tokens only.
*
* @param {string} token - The JSON web token to check
* @returns {boolean} Whether the token has expired.
* Returns `false` if the token cannot be decoded or an expire time is not specified in the payload.
* @returns {boolean}
* Whether the token has expired. Returns `false` if the token cannot
* be decoded or an expire time is not specified in the payload.
*/
public static hasTokenExpired(token: string): boolean {
// JWT format: [header].[payload].[signature]
Expand Down
4 changes: 1 addition & 3 deletions packages/imperative/src/config/src/ConvertV1Profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,7 @@ export class ConvertV1Profiles {
// Initialization for VSCode extensions does not create the config property, so create it now.
ImperativeConfig.instance.config = await Config.load(
ImperativeConfig.instance.loadedConfig.name,
{
homeDir: ImperativeConfig.instance.loadedConfig.defaultHome
}
{ homeDir: ImperativeConfig.instance.cliHome }
);
}

Expand Down
34 changes: 21 additions & 13 deletions packages/imperative/src/config/src/ProfileCredentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@

/**
* Check if secure credentials will be encrypted or stored in plain text.
* If using team config, this will always return true. If using classic
* profiles, this will check whether a custom CredentialManager is defined
* in the Imperative settings.json file.
* This will return true if the team configuration files contain secure
* fields, or if a custom CredentialManager is defined in the Imperative
* settings.json file.
*/
public get isSecured(): boolean {
this.mSecured = this.isTeamConfigSecure() || this.isCredentialManagerInAppSettings();
Expand All @@ -74,6 +74,22 @@
throw new ImperativeError({ msg: "Secure credential storage is not enabled" });
}

await this.activateCredMgrOverride();
await this.mProfileInfo.getTeamConfig().api.secure.load({
load: (key: string): Promise<string> => {
return CredentialManagerFactory.manager.load(key, true);

Check warning on line 80 in packages/imperative/src/config/src/ProfileCredentials.ts

View check run for this annotation

Codecov / codecov/patch

packages/imperative/src/config/src/ProfileCredentials.ts#L79-L80

Added lines #L79 - L80 were not covered by tests
},
save: (key: string, value: any): Promise<void> => {
return CredentialManagerFactory.manager.save(key, value);

Check warning on line 83 in packages/imperative/src/config/src/ProfileCredentials.ts

View check run for this annotation

Codecov / codecov/patch

packages/imperative/src/config/src/ProfileCredentials.ts#L82-L83

Added lines #L82 - L83 were not covered by tests
}
});
}

/**
* Attempt to initialize `CredentialManagerFactory` with the specified override.
* @internal
*/
public async activateCredMgrOverride(): Promise<void> {
if (!CredentialManagerFactory.initialized) {
try {
// TODO? Make CredentialManagerFactory.initialize params optional
Expand All @@ -86,15 +102,6 @@
});
}
}

await this.mProfileInfo.getTeamConfig().api.secure.load({
load: (key: string): Promise<string> => {
return CredentialManagerFactory.manager.load(key, true);
},
save: (key: string, value: any): Promise<void> => {
return CredentialManagerFactory.manager.save(key, value);
}
});
}

/**
Expand All @@ -109,8 +116,9 @@
/**
* Check whether a custom CredentialManager is defined in the Imperative
* settings.json file.
* @internal
*/
private isCredentialManagerInAppSettings(): boolean {
public isCredentialManagerInAppSettings(): boolean {
try {
const fileName = path.join(ImperativeConfig.instance.cliHome, "settings", "imperative.json");
let settings: any;
Expand Down
Loading