Skip to content
This repository has been archived by the owner on Nov 13, 2023. It is now read-only.

Handle new APIML unique cookie identifier #996

Merged
merged 28 commits into from
Jul 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f7369a9
add ImperativeExpect.toMatchRegExp
zFernand0 Jun 23, 2023
323858f
prevent multiple logout from failing
zFernand0 Jun 23, 2023
a5c432a
allow autoStore for dynamic apiml token types
zFernand0 Jun 26, 2023
45e775a
allow logout to remove token value event if token type is not defined…
zFernand0 Jun 26, 2023
edcce76
add utility method to better support nested configuration
zFernand0 Jun 26, 2023
d76da65
Add support for multiple tokenType authentication processes in a sing…
zFernand0 Jun 26, 2023
4aa4547
prevent auto-init from performing a second login operation if we alre…
zFernand0 Jun 26, 2023
274c368
fix lint issue and revert one scenario
zFernand0 Jun 28, 2023
fcad313
fix unit and integration test
zFernand0 Jun 28, 2023
f98f819
add unit tests
zFernand0 Jun 29, 2023
433cea2
Prevent a breaking change on multiple logout operations with V1 profiles
zFernand0 Jun 30, 2023
71cb266
update changelog
zFernand0 Jun 30, 2023
056cb00
Merge branch 'master' of https://github.com/zowe/imperative into fix-979
zFernand0 Jun 30, 2023
9571e98
address PR comments
zFernand0 Jul 5, 2023
c248e16
addres PR comments
zFernand0 Jul 5, 2023
1b33b86
Address PR comment
zFernand0 Jul 7, 2023
1f707fc
prevent throwing an error if no credentials are provided
zFernand0 Jul 5, 2023
ddb2d45
avoid prompting for tokenValue if no creds are provided
zFernand0 Jul 5, 2023
a7167f8
fix unit tests
zFernand0 Jul 10, 2023
54319c8
add basePath check back to prevent ze issues
zFernand0 Jul 11, 2023
528896c
move press enter to skip to a constant value
zFernand0 Jul 11, 2023
262f4fc
force user-pass to take precedence over tokens in the abstract rest c…
zFernand0 Jul 11, 2023
8a0f56e
fix code smell and sonar bug
zFernand0 Jul 12, 2023
9d79c84
make sure to use the same tokentype if provided + remove creds as soo…
zFernand0 Jul 20, 2023
3cf8573
fix unit tests and make sure that the ISession doesn't have creds if …
zFernand0 Jul 20, 2023
c89b191
display better error messages on logout
zFernand0 Jul 24, 2023
f4c6c41
Merge branch 'master' into fix-979
zFernand0 Jul 24, 2023
96a5c6a
fix wording of error message
zFernand0 Jul 25, 2023
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

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

## Recent Changes

- Enhancement: Handled unique cookie identifier in the form of dynamic token types. [#966](https://github.com/zowe/imperative/pull/996)
- Enhancement: Added a new utility method to `ImperativeExpect` to match regular expressions. [#966](https://github.com/zowe/imperative/pull/996)
- Enhancement: Added support for multiple login operations in a single `config secure` command execution. [#966](https://github.com/zowe/imperative/pull/996)
- BugFix: Allowed for multiple `auth logout` operations. [#966](https://github.com/zowe/imperative/pull/996)
- BugFix: Prevented `auto-init` from sending two `login` requests to the server. [#966](https://github.com/zowe/imperative/pull/996)

## `5.15.1`

- BugFix: Enabled NextVerFeatures.useV3ErrFormat() to form the right environment variable name even if Imperative.init() has not been called.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ import * as keytar from "keytar";
let TEST_ENVIRONMENT: ITestEnvironment;
describe("imperative-test-cli auth logout", () => {
async function loadSecureProp(profileName: string): Promise<string> {
const securedValue = await keytar.getPassword("imperative-test-cli", "secure_config_props");
const securedValue = await keytar.getPassword("imperative-test-cli", "secure_config_props") as string;
const securedValueJson = JSON.parse(Buffer.from(securedValue, "base64").toString());
return Object.values(securedValueJson)[0][`profiles.${profileName}.properties.tokenValue`];
return Object.values(securedValueJson)[0]?.[`profiles.${profileName}.properties.tokenValue`];
}

// Create the unique test environment
Expand Down Expand Up @@ -121,6 +121,7 @@ describe("imperative-test-cli auth logout", () => {
it("should have auth logout command that invalidates another token", async () => {
let response = runCliScript(__dirname + "/__scripts__/base_profile_and_auth_login_config_local.sh",
TEST_ENVIRONMENT.workingDir + "/testDir", ["fakeUser", "fakePass"]);
expect(response.error).toBeFalsy();
expect(response.stderr.toString()).toBe("");
expect(response.status).toBe(0);

Expand Down
24 changes: 24 additions & 0 deletions packages/config/__tests__/Config.api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,30 @@ describe("Config API tests", () => {
expect(profile).toBeNull();
});
});
describe("expandPath", () => {
it("should expand a short proeprty path", async () => {
const config = await Config.load(MY_APP);
const profilePath = "lpar1.zosmf";
expect(config.api.profiles.expandPath(profilePath)).toEqual("profiles.lpar1.profiles.zosmf");
});
it("should expand a path with the keyword profiles", async () => {
const config = await Config.load(MY_APP);
const profilePath = "profiles.zosmf";
expect(config.api.profiles.expandPath(profilePath)).toEqual("profiles.profiles.profiles.zosmf");
});
});
describe("getProfileNameFromPath", () => {
it("should shrink profile paths", async () => {
const config = await Config.load(MY_APP);
const propertyPath = "profiles.lpar1.profiles.zosmf.properties.host";
expect(config.api.profiles.getProfileNameFromPath(propertyPath)).toEqual("lpar1.zosmf");
});
it("should shrink profile paths with the keyword profiles", async () => {
const config = await Config.load(MY_APP);
const propertyPath = "profiles.profiles.profiles.zosmf.properties.host";
expect(config.api.profiles.getProfileNameFromPath(propertyPath)).toEqual("profiles.zosmf");
});
});
});
describe("plugins", () => {
describe("get", () => {
Expand Down
37 changes: 36 additions & 1 deletion packages/config/__tests__/ConfigAutoStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,41 @@ describe("ConfigAutoStore tests", () => {
expect(authHandler instanceof AbstractAuthHandler).toBe(true);
});

it("should be able to find auth handler for base profile with a dynamic APIML token type", async () => {
await setupConfigToLoad({
profiles: {
base: {
type: "base",
properties: {
tokenType: SessConstants.TOKEN_TYPE_APIML + ".1"
}
}
},
defaults: { base: "base" }
});

const authHandler = ConfigAutoStore.findAuthHandlerForProfile("profiles.base", {} as any);
expect(authHandler).toBeDefined();
expect(authHandler instanceof AbstractAuthHandler).toBe(true);
});

it("should not be able to find auth handler for base profile with a dynamic JWT token type", async () => {
await setupConfigToLoad({
profiles: {
base: {
type: "base",
properties: {
tokenType: SessConstants.TOKEN_TYPE_JWT + ".1"
}
}
},
defaults: { base: "base" }
});

const authHandler = ConfigAutoStore.findAuthHandlerForProfile("profiles.base", {} as any);
expect(authHandler).toBeUndefined();
});

it("should be able to find auth handler for service profile", async () => {
await setupConfigToLoad({
profiles: {
Expand Down Expand Up @@ -132,7 +167,7 @@ describe("ConfigAutoStore tests", () => {
expect(authHandler).toBeUndefined();
});

it("should not find auth handler if profile base path is undefined", async () => {
it("should not find auth handler if service profile base path is undefined", async () => {
await setupConfigToLoad({
profiles: {
base: {
Expand Down
22 changes: 12 additions & 10 deletions packages/config/src/ConfigAutoStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { AbstractAuthHandler } from "../../imperative/src/auth/handlers/Abstract
import { ImperativeConfig } from "../../utilities";
import { ISession } from "../../rest/src/session/doc/ISession";
import { Session } from "../../rest/src/session/Session";
import { AUTH_TYPE_TOKEN } from "../../rest/src/session/SessConstants";
import { AUTH_TYPE_TOKEN, TOKEN_TYPE_APIML } from "../../rest/src/session/SessConstants";
import { Logger } from "../../logger";
import {
IConfigAutoStoreFindActiveProfileOpts,
Expand Down Expand Up @@ -78,18 +78,19 @@ export class ConfigAutoStore {
const profileType = lodash.get(config.properties, `${opts.profilePath}.type`);
const profile = config.api.profiles.get(opts.profilePath.replace(/profiles\./g, ""), false);

if (profile == null || profileType == null) { // Profile must exist and have type defined
if (profile == null || profileType == null) { // Profile must exist and have type defined
return;
} else if (profileType === "base") {
if (profile.tokenType == null) { // Base profile must have tokenType defined
if (profile.tokenType == null) { // Base profile must have tokenType defined
return;
}
} else {
if (profile.basePath == null) { // Service profiles must have basePath defined
if (profile.basePath == null) { // Service profiles must have basePath defined
return;
} else if (profile.tokenType == null) { // If tokenType undefined in service profile, fall back to base profile
}
if (profile.tokenType == null) { // If tokenType undefined in service profile, fall back to base profile
const baseProfileName = ConfigUtils.getActiveProfileName("base", opts.cmdArguments, opts.defaultBaseProfileName);
return this._findAuthHandlerForProfile({ ...opts, profilePath: config.api.profiles.expandPath(baseProfileName) });
return this._findAuthHandlerForProfile({ ...opts, profilePath: config.api.profiles.getProfilePathFromName(baseProfileName) });
}
}

Expand All @@ -106,7 +107,7 @@ export class ConfigAutoStore {

if (authHandlerClass instanceof AbstractAuthHandler) {
const { promptParams } = authHandlerClass.getAuthHandlerApi();
if (profile.tokenType === promptParams.defaultTokenType) {
if (profile.tokenType === promptParams.defaultTokenType || profile.tokenType.startsWith(TOKEN_TYPE_APIML)) {
return authHandlerClass; // Auth service must have matching token type
}
}
Expand Down Expand Up @@ -140,7 +141,7 @@ export class ConfigAutoStore {
return;
}
const [profileType, profileName] = profileData ?? [opts.profileType, opts.profileName];
const profilePath = config.api.profiles.expandPath(profileName);
const profilePath = config.api.profiles.getProfilePathFromName(profileName);

// Replace user and password with tokenValue if tokenType is defined in config
if (profileProps.includes("user") && profileProps.includes("password") && await this._fetchTokenForSessCfg({ ...opts, profilePath })) {
Expand Down Expand Up @@ -179,7 +180,7 @@ export class ConfigAutoStore {
(propName === "tokenValue" && profileObj.tokenType == null && baseProfileObj.tokenType != null ||
profileType === "base")
) {
propProfilePath = config.api.profiles.expandPath(baseProfileName);
propProfilePath = config.api.profiles.getProfilePathFromName(baseProfileName);
isSecureProp = baseProfileSchema.properties[propName].secure || baseProfileSecureProps.includes(propName);
}

Expand Down Expand Up @@ -235,7 +236,7 @@ export class ConfigAutoStore {

const api = authHandlerClass.getAuthHandlerApi();
opts.sessCfg.type = AUTH_TYPE_TOKEN;
opts.sessCfg.tokenType = api.promptParams.defaultTokenType;
opts.sessCfg.tokenType = opts.params?.arguments?.tokenType ?? api.promptParams.defaultTokenType;
const baseSessCfg: ISession = { type: opts.sessCfg.type };

for (const propName of Object.keys(ImperativeConfig.instance.loadedConfig.baseProfile.schema.properties)) {
Expand All @@ -247,6 +248,7 @@ export class ConfigAutoStore {

Logger.getAppLogger().info(`Fetching ${opts.sessCfg.tokenType} for ${opts.profilePath}`);
opts.sessCfg.tokenValue = await api.sessionLogin(new Session(baseSessCfg));
opts.sessCfg.user = opts.sessCfg.password = undefined;
return true;
}
}
5 changes: 5 additions & 0 deletions packages/config/src/ConfigConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,9 @@ export class ConfigConstants {
* ID used for storing secure credentials in vault
*/
public static readonly SECURE_ACCT = "secure_config_props";

/**
* ID used for storing secure credentials in vault
*/
public static readonly SKIP_PROMPT = "- Press ENTER to skip: ";
}
4 changes: 2 additions & 2 deletions packages/config/src/ProfileInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ export class ProfileInfo {
const foundProfNm = configProperties.defaults[profileType];

// for a team config, we use the last node of the jsonLoc as the name
const foundJson = this.mLoadedConfig.api.profiles.expandPath(foundProfNm);
const foundJson = this.mLoadedConfig.api.profiles.getProfilePathFromName(foundProfNm);
const teamOsLocation: string[] = this.findTeamOsLocation(foundJson);

// assign the required poperties to defaultProfile
Expand Down Expand Up @@ -1330,7 +1330,7 @@ export class ProfileInfo {
* @param opts Set of options that allow this method to get the profile location
*/
private argTeamConfigLoc(opts: IArgTeamConfigLoc): [IProfLoc, boolean] {
const segments = this.mLoadedConfig.api.profiles.expandPath(opts.profileName).split(".profiles.");
const segments = this.mLoadedConfig.api.profiles.getProfilePathFromName(opts.profileName).split(".profiles.");
let osLocInfo: IProfLocOsLocLayer;
if (opts.osLocInfo?.user != null || opts.osLocInfo?.global != null)
osLocInfo = { user: opts.osLocInfo?.user, global: opts.osLocInfo?.global };
Expand Down
2 changes: 1 addition & 1 deletion packages/config/src/api/ConfigLayers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ export class ConfigLayers extends ConfigApi {
* @returns User and global properties, or undefined if profile does not exist
*/
public find(profileName: string): { user: boolean, global: boolean } {
const profilePath = this.mConfig.api.profiles.expandPath(profileName);
const profilePath = this.mConfig.api.profiles.getProfilePathFromName(profileName);
for (const layer of this.mConfig.layers) {
if (lodash.get(layer.properties, profilePath) != null) {
return layer;
Expand Down
37 changes: 37 additions & 0 deletions packages/config/src/api/ConfigProfiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,48 @@ export class ConfigProfiles extends ConfigApi {
*
* @returns The expanded path.
*
* @deprecated Please use getProfilePathFromName
*/
public expandPath(shortPath: string): string {
return this.getProfilePathFromName(shortPath);
}

// _______________________________________________________________________
/**
* Expands a short path into an expanded path.
*
* @param shortPath The short path.
*
* @returns The expanded path.
*/
public getProfilePathFromName(shortPath: string): string {
return shortPath.replace(/(^|\.)/g, "$1profiles.");
}

// _______________________________________________________________________
/**
* Obtain the profile name (either nested or not) based on a property path.
*
* @param path The property path.
*
* @returns The corresponding profile name.
*
* @note This may be useful for supporting token authentication in a nested configuration
*
*/
public getProfileNameFromPath(path: string): string {
zFernand0 marked this conversation as resolved.
Show resolved Hide resolved
let profileName = "";
const segments = path.split(".");
for (let i = 0; i < segments.length; i++) {
const p = segments[i];
if (p === "properties") break;
if (i%2) {
profileName += profileName.length > 0 ? "." + p : p;
}
}
return profileName;
}

// _______________________________________________________________________
/**
* Build the set of properties contained within a set of nested profiles.
Expand Down
2 changes: 1 addition & 1 deletion packages/config/src/api/ConfigSecure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export class ConfigSecure extends ConfigApi {
* @returns Array of secure property names
*/
public securePropsForProfile(profileName: string) {
const profilePath = this.mConfig.api.profiles.expandPath(profileName);
const profilePath = this.mConfig.api.profiles.getProfilePathFromName(profileName);
const secureProps = [];
for (const propPath of this.secureFields()) {
const pathSegments = propPath.split("."); // profiles.XXX.properties.YYY
Expand Down
20 changes: 20 additions & 0 deletions packages/expect/__tests__/ImperativeExpect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,26 @@ const nestedObj = {
};

describe("ImperativeExpect tests", () => {
describe("toMatchRegExp", () => {
it("should not throw an error if the value matches the provided regular expression", () => {
let error: ImperativeError = {} as any;
try {
ImperativeExpect.toMatchRegExp("token", "^token$");
} catch(thrownError) {
error = thrownError;
}
expect(error).toEqual({});
});
it("should throw an error if the value does not match the provided regular expression with custom message", () => {
let error: ImperativeError = {} as any;
try {
ImperativeExpect.toMatchRegExp("token", "^token1", "test");
} catch(thrownError) {
error = thrownError;
}
expect(error.message).toContain("test");
});
});

it("Should throw an error for an undefined key when we expect it to be defined", () => {
let error: ImperativeError;
Expand Down
15 changes: 15 additions & 0 deletions packages/expect/src/ImperativeExpect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,21 @@ export class ImperativeExpect {
}
}

/**
* Expect that value matches the regular expression (via ".test()" method).
* @static
* @param {*} value - Value
* @param {*} myRegex - Regular expression
* @param {string} [msg] - The message to throw - overrides the default message
* @memberof ImperativeExpect
*/
public static toMatchRegExp(value: any, myRegex: string, msg?: string) {
if (!(new RegExp(myRegex).test(value))) {
throw new ImperativeError({msg: msg || "Input object/value does not match the regular expression"},
{tag: ImperativeExpect.ERROR_TAG});
}
}

/**
* Expect the object passed to be defined.
* @static
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import * as jestDiff from "jest-diff";
import stripAnsi = require("strip-ansi");
import { ConfigSchema } from "../../../../../config";
import { CredentialManagerFactory } from "../../../../../security";
import { SessConstants } from "../../../../../rest";
import { SessConstants, Session } from "../../../../../rest";
import { OverridesLoader } from "../../../../src/OverridesLoader";

jest.mock("strip-ansi");
Expand Down Expand Up @@ -122,11 +122,18 @@ describe("BaseAutoInitHandler", () => {

it("should call init with token", async () => {
const handler = new FakeAutoInitHandler();
handler.createSessCfgFromArgs = jest.fn().mockReturnValue({
hostname: "fakeHost", port: 3000, tokenValue: "fake"
});
const params: IHandlerParameters = {
...mockParams,
arguments: {
tokenType: "fake",
tokenValue: "fake"
tokenValue: "fake",
user: "toBeDeleted",
password: "toBeDeleted",
cert: "toBeDeleted",
certKey: "toBeDeleted"
}
} as any;

Expand Down Expand Up @@ -158,8 +165,15 @@ describe("BaseAutoInitHandler", () => {
caughtError = error;
}

const mSession: Session = doInitSpy.mock.calls[0][0] as any;

expect(caughtError).toBeUndefined();
expect(doInitSpy).toBeCalledTimes(1);
expect(mSession.ISession.user).toBeUndefined();
expect(mSession.ISession.password).toBeUndefined();
expect(mSession.ISession.base64EncodedAuth).toBeUndefined();
expect(mSession.ISession.cert).toBeUndefined();
expect(mSession.ISession.certKey).toBeUndefined();
expect(processAutoInitSpy).toBeCalledTimes(1);
expect(createSessCfgFromArgsSpy).toBeCalledTimes(1);
expect(mockConfigApi.layers.merge).toHaveBeenCalledTimes(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { ISession, AbstractSession } from "../../../../../../rest";
export class FakeAutoInitHandler extends BaseAutoInitHandler {
public mProfileType: string = "fruit";

protected createSessCfgFromArgs(args: ICommandArguments): ISession {
public createSessCfgFromArgs(args: ICommandArguments): ISession {
return { hostname: "fakeHost", port: 3000 };
}

Expand Down
Loading