diff --git a/packages/imperative/src/censor/__tests__/Censor.unit.test.ts b/packages/imperative/src/censor/__tests__/Censor.unit.test.ts index 534eb3038..f45c655bf 100644 --- a/packages/imperative/src/censor/__tests__/Censor.unit.test.ts +++ b/packages/imperative/src/censor/__tests__/Censor.unit.test.ts @@ -16,6 +16,7 @@ import { EnvironmentalVariableSettings } from "../../imperative/src/env/Environm import { Censor } from "../src/Censor"; import { ImperativeConfig } from "../../utilities/src/ImperativeConfig"; import { ICensorOptions } from "../.."; +import { ConfigSecure } from "../../config/src/api"; beforeAll(() => { (Censor as any).mSchema = null; @@ -537,7 +538,7 @@ describe("Censor tests", () => { it("should reset the censored options when called with nothing", () => { (Censor as any).mConfig = "bad"; - (Censor as any).setCensoredOptions({}); + Censor.setCensoredOptions({}); expect((Censor as any).mConfig).not.toEqual("bad"); }); @@ -746,5 +747,194 @@ describe("Censor tests", () => { expect(Censor.CENSORED_OPTIONS).not.toContain("test2"); expect(Censor.CENSORED_OPTIONS).not.toContain("t"); }); + + it("should apply a config without command definitions", () => { + const censorOpts: ICensorOptions = { + config: { + api: { + secure: new ConfigSecure({} as any) + }, + mProperties: { + profiles: { + test1: { + secure: [ + "host", + "port", + "user" + ] + } + } + } + } as any + } + Censor.setCensoredOptions(censorOpts); + + expect(Censor.CENSORED_OPTIONS).toContain("host"); + expect(Censor.CENSORED_OPTIONS).toContain("port"); + expect(Censor.CENSORED_OPTIONS).toContain("user"); + expect(Censor.CENSORED_OPTIONS).toContain("password"); + }); + + it("should apply a config with command definitions 1", () => { + const profile = { + test1: { + secure: [ + "host", + "port", + "user" + ] + } + } + const censorOpts: ICensorOptions = { + config: { + api: { + secure: new ConfigSecure({ + api: { + profiles: { + getProfilePathFromName: jest.fn().mockReturnValue("profiles.test1") + } + }, + mProperties: { + profiles: { + ...profile + } + } + } as any), + profiles: { + get: jest.fn().mockReturnValue(profile) + } + }, + mProperties: { + profiles: { + ...profile + } + } + } as any, + commandDefinition: { + profile: { + required: [ + "test" + ] + } + } as any, + commandArguments: { + "test-profile": "test1" + } as any + } + Censor.setCensoredOptions(censorOpts); + + expect(Censor.CENSORED_OPTIONS).toContain("host"); + expect(Censor.CENSORED_OPTIONS).toContain("port"); + expect(Censor.CENSORED_OPTIONS).toContain("user"); + expect(Censor.CENSORED_OPTIONS).toContain("password"); + }); + + it("should apply a config with command definitions 2", () => { + const profile = { + test1: { + secure: [ + "host", + "port", + "user" + ] + } + } + const censorOpts: ICensorOptions = { + config: { + api: { + secure: new ConfigSecure({ + api: { + profiles: { + getProfilePathFromName: jest.fn().mockReturnValue("profiles.test1") + } + }, + mProperties: { + profiles: { + ...profile + } + } + } as any), + profiles: { + get: jest.fn().mockReturnValue(profile) + } + }, + mProperties: { + profiles: { + ...profile + } + } + } as any, + commandDefinition: { + profile: { + optional: [ + "test" + ] + } + } as any, + commandArguments: { + "test-profile": "test1" + } as any + } + Censor.setCensoredOptions(censorOpts); + + expect(Censor.CENSORED_OPTIONS).toContain("host"); + expect(Censor.CENSORED_OPTIONS).toContain("port"); + expect(Censor.CENSORED_OPTIONS).toContain("user"); + expect(Censor.CENSORED_OPTIONS).toContain("password"); + }); + + it("should not apply a config with command definitions if the profile does not apply", () => { + const profile = { + test1: { + secure: [ + "host", + "port", + "user" + ] + } + } + const censorOpts: ICensorOptions = { + config: { + api: { + secure: new ConfigSecure({ + api: { + profiles: { + getProfilePathFromName: jest.fn().mockReturnValue("profiles.test1") + } + }, + mProperties: { + profiles: { + ...profile + } + } + } as any), + profiles: { + get: jest.fn().mockReturnValue(profile) + } + }, + mProperties: { + profiles: { + ...profile + } + } + } as any, + commandDefinition: { + profile: { + required: [ + "nottest" + ] + } + } as any, + commandArguments: { + "test-profile": "test1" + } as any + } + Censor.setCensoredOptions(censorOpts); + + expect(Censor.CENSORED_OPTIONS).not.toContain("host"); + expect(Censor.CENSORED_OPTIONS).not.toContain("port"); + expect(Censor.CENSORED_OPTIONS).not.toContain("user"); + expect(Censor.CENSORED_OPTIONS).toContain("password"); + }); }); }); diff --git a/packages/imperative/src/censor/src/Censor.ts b/packages/imperative/src/censor/src/Censor.ts index 0ee055265..56b03adef 100644 --- a/packages/imperative/src/censor/src/Censor.ts +++ b/packages/imperative/src/censor/src/Censor.ts @@ -20,6 +20,7 @@ import { EnvironmentalVariableSettings } from "../../imperative/src/env/Environm import { ICommandProfileTypeConfiguration } from "../../cmd/src/doc/profiles/definition/ICommandProfileTypeConfiguration"; import { IProfileSchema} from "../../profiles/src/doc/definition/IProfileSchema"; import { IProfileTypeConfiguration } from "../../profiles/src/doc/config/IProfileTypeConfiguration"; +import { ICommandArguments, ICommandDefinition } from "../../cmd"; export class Censor { @@ -64,14 +65,16 @@ export class Censor { // Set a censored options list that can be set and retrieved for each command. private static censored_options: Set = new Set(this.DEFAULT_CENSORED_OPTIONS); - // Keep a cached config object if provided in another function - private static mConfig: Config = null; - // Return a customized list of censored options (or just the defaults if not set). public static get CENSORED_OPTIONS(): string[] { return Array.from(this.censored_options); } + //Singleton caches of the Config, Command Definition and Command Arguments + private static mConfig: Config = null; + private static mCommandDefinition: ICommandDefinition = null; + private static mCommandArguments: ICommandArguments = null; + /** * Singleton implementation of an internal reference to the schema */ @@ -184,6 +187,15 @@ export class Censor { return false; } + /** + * Identifies if a property is a secure property + * @param {string} prop - The property to check + * @returns {boolean} - True if the property is secure; False otherwise + */ + public static isSecureValue(prop: string) { + return this.SECURE_PROMPT_OPTIONS.includes(prop); + } + /**************************************************************************************** * Bread and butter functions, setting up the class and performing censorship of values * ****************************************************************************************/ @@ -216,29 +228,10 @@ export class Censor { // Include any secure options from the config if (censorOpts.config) { - // Try to use the command and inputs to find the profiles being loaded - if (censorOpts.commandDefinition && censorOpts.commandArguments) { - const profiles = []; - for (const prof of censorOpts.commandDefinition.profile?.required || []) { - profiles.push(prof); - } - for (const prof of censorOpts.commandDefinition.profile?.optional || []) { - profiles.push(prof); - } + this.mCommandArguments = censorOpts.commandArguments; + this.mCommandDefinition = censorOpts.commandDefinition; - for (const prof of profiles) { - // If the profile exists, append all of the secure props to the censored list - const profName = censorOpts.commandArguments?.[`${prof}-profile`]; - if (profName && censorOpts.config.api.profiles.get(profName)) { - censorOpts.config.api.secure.securePropsForProfile(profName).forEach(prop => this.addCensoredOption(prop)); - } - } - } else { - // We only have a configuration file, assume every property that is secured should be censored - censorOpts.config.api.secure.findSecure(censorOpts.config.mProperties.profiles, "profiles").forEach( - prop => this.addCensoredOption(prop.split(".").pop()) - ); - } + this.censorFromCommandOrConfig(); } } else if (this.profileSchemas) { for (const profileType of this.profileSchemas) { @@ -247,6 +240,40 @@ export class Censor { } } + /** + * Censor from the command cache from setCensoredOptions, or use the user's config to censor anything secure + */ + private static censorFromCommandOrConfig() { + const config = this.mConfig ?? ImperativeConfig.instance?.config; + + if (config) { + // Try to use the command and inputs to find the profiles being loaded + if (this.mCommandDefinition && this.mCommandArguments) { + const profiles = []; + for (const prof of this.mCommandDefinition.profile?.required || []) { + profiles.push(prof); + } + for (const prof of this.mCommandDefinition.profile?.optional || []) { + profiles.push(prof); + } + + for (const prof of profiles) { + // If the profile exists, append all of the secure props to the censored list + let profName = this.mCommandArguments?.[`${prof}-profile`]; + if (!profName) { profName = this.mConfig.mProperties.defaults[`${prof}`]; } + if (profName && config.api.profiles.get(profName)) { + config.api.secure.securePropsForProfile(profName).forEach(prop => this.addCensoredOption(prop)); + } + } + } else { + // We only have a configuration file, assume every property that is secured should be censored + config.api.secure.findSecure(config.mProperties.profiles, "profiles").forEach( + prop => this.addCensoredOption(prop.split(".").pop()) + ); + } + } + } + /** * Copy and censor any sensitive CLI arguments before logging/printing * @param {string[]} args - The args list to censor @@ -286,10 +313,12 @@ export class Censor { let newData = data; + this.censorFromCommandOrConfig(); + const secureFields = config.api.secure.findSecure(config.mProperties.profiles, "profiles"); for (const prop of secureFields) { const sec = lodash.get(config.mProperties, prop); - if (sec && typeof sec !== "object" && !this.isSpecialValue(prop)) { + if (sec && typeof sec !== "object" && !this.isSpecialValue(prop) && this.isSecureValue(prop.split(".").pop())) { newData = newData.replace(new RegExp(sec, "gi"), this.CENSOR_RESPONSE); } }