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

feat(v3): Support logging in to multiple API ML instances #3019

Merged
merged 17 commits into from
Aug 20, 2024
Merged
Changes from 9 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
2 changes: 2 additions & 0 deletions packages/zowe-explorer-api/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -39,6 +39,8 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t
- Added the `onVaultUpdate` VSCode event to notify extenders when credentials are updated on the OS vault by other applications. [#2994](https://github.com/zowe/zowe-explorer-vscode/pull/2994)
- Added the `onCredMgrsUpdate` VSCode event to notify extenders when the local PC's credential manager has been updated by other applications. [#2994](https://github.com/zowe/zowe-explorer-vscode/pull/2994)
- **Breaking:** Updated most function signatures for exported programmatic interfaces. Changes make developing with the Zowe Explorer API more efficient for extenders by showing which properties they can expect when calling our APIs. [#2952](https://github.com/zowe/zowe-explorer-vscode/issues/2952)
- Added support for logging in to multiple API ML instances per team config file. [#2264](https://github.com/zowe/zowe-explorer-vscode/issues/2264)
- **Next Breaking:** Changed return type of `ZoweVsCodeExtension.logoutWithBaseProfile` method from `void` to `boolean` to indicate whether logout was successful.

### Bug fixes

Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ import * as fs from "fs";
import * as imperative from "@zowe/imperative";
import { ProfilesCache } from "../../../src/profiles/ProfilesCache";
import { FileManagement, Types } from "../../../src";
import { mocked } from "../../../__mocks__/mockUtils";

jest.mock("fs");

@@ -70,7 +71,7 @@ const baseProfileWithToken = {
tokenValue: "baseToken",
},
};
const profilemetadata: imperative.ICommandProfileTypeConfiguration[] = [
const profileMetadata: imperative.ICommandProfileTypeConfiguration[] = [
{
type: "acme",
schema: {
@@ -83,6 +84,13 @@ const profilemetadata: imperative.ICommandProfileTypeConfiguration[] = [
];

function createProfInfoMock(profiles: Partial<imperative.IProfileLoaded>[]): imperative.ProfileInfo {
const teamConfigApi: Partial<imperative.Config> = {
api: {
profiles: { get: jest.fn() },
secure: { securePropsForProfile: jest.fn().mockReturnValue([]) },
} as any,
exists: true,
};
return {
getAllProfiles: (profType?: string) =>
profiles
@@ -113,7 +121,7 @@ function createProfInfoMock(profiles: Partial<imperative.IProfileLoaded>[]): imp
knownArgs: Object.entries(profile.profile as object).map(([k, v]) => ({ argName: k, argValue: v as unknown })),
};
},
getTeamConfig: () => ({ exists: true }),
getTeamConfig: () => teamConfigApi,
updateProperty: jest.fn(),
updateKnownProperty: jest.fn(),
isSecured: jest.fn(),
@@ -157,16 +165,16 @@ describe("ProfilesCache", () => {

it("addToConfigArray should set the profileTypeConfigurations array", () => {
const profCache = new ProfilesCache(fakeLogger as unknown as imperative.Logger);
profilemetadata.push(profilemetadata[0]);
profCache.addToConfigArray(profilemetadata);
expect(profCache.profileTypeConfigurations).toEqual(profilemetadata.filter((a, index) => index == 0));
profileMetadata.push(profileMetadata[0]);
profCache.addToConfigArray(profileMetadata);
expect(profCache.profileTypeConfigurations).toEqual(profileMetadata.filter((a, index) => index == 0));
});

it("getConfigArray should return the data of profileTypeConfigurations Array", () => {
const profCache = new ProfilesCache(fakeLogger as unknown as imperative.Logger);
profCache.profileTypeConfigurations = profilemetadata;
profCache.profileTypeConfigurations = profileMetadata;
const res = profCache.getConfigArray();
expect(res).toEqual(profilemetadata);
expect(res).toEqual(profileMetadata);
});

it("loadNamedProfile should find profiles by name and type", () => {
@@ -537,6 +545,29 @@ describe("ProfilesCache", () => {
expect(profile).toMatchObject(baseProfile);
});

it("fetchBaseProfile should return typeless profile if base profile not found", async () => {
const profCache = new ProfilesCache(fakeLogger as unknown as imperative.Logger);
jest.spyOn(profCache, "getProfileInfo").mockResolvedValue(createProfInfoMock([]));
const profile = await profCache.fetchBaseProfile("lpar1.zosmf");
expect(profile).toMatchObject({ name: "lpar1", type: "base" });
});

it("fetchBaseProfile should return typeless profile if base profile does not contain token value", async () => {
const profCache = new ProfilesCache(fakeLogger as unknown as imperative.Logger);
jest.spyOn(profCache, "getProfileInfo").mockResolvedValue(createProfInfoMock([baseProfile]));
const profile = await profCache.fetchBaseProfile("lpar1.zosmf");
expect(profile).toMatchObject({ name: "lpar1", type: "base" });
});

it("fetchBaseProfile should return base profile if it contains token value", async () => {
const profCache = new ProfilesCache(fakeLogger as unknown as imperative.Logger);
const profInfoMock = createProfInfoMock([baseProfile]);
jest.spyOn(profCache, "getProfileInfo").mockResolvedValue(profInfoMock);
mocked(profInfoMock.getTeamConfig().api.secure.securePropsForProfile).mockReturnValue(["tokenValue"]);
const profile = await profCache.fetchBaseProfile("lpar1.zosmf");
expect(profile).toMatchObject(baseProfile);
});

it("fetchBaseProfile should return undefined if base profile not found", async () => {
const profCache = new ProfilesCache(fakeLogger as unknown as imperative.Logger);
jest.spyOn(profCache, "getProfileInfo").mockResolvedValue(createProfInfoMock([lpar1Profile]));
Original file line number Diff line number Diff line change
@@ -121,7 +121,7 @@ describe("ZoweVsCodeExtension", () => {
allProfiles,
allExternalTypes: [],
fetchBaseProfile: jest.fn(),
loadNamedProfile: jest.fn().mockReturnValue({ profile: testProfile }),
loadNamedProfile: jest.fn().mockReturnValue(serviceProfile),
updateBaseProfileFileLogin: jest.fn(),
updateBaseProfileFileLogout: jest.fn(),
getLoadedProfConfig: jest.fn().mockReturnValue({ profile: {} }),
@@ -139,16 +139,20 @@ describe("ZoweVsCodeExtension", () => {
});

it("should not login if the base profile cannot be fetched", async () => {
const errorMessageSpy = jest.spyOn(Gui, "errorMessage");
testCache.fetchBaseProfile.mockResolvedValue(null);
await ZoweVsCodeExtension.loginWithBaseProfile("service");
expect(testCache.fetchBaseProfile).toHaveBeenCalledTimes(1);
expect(testCache.updateBaseProfileFileLogin).not.toHaveBeenCalled();
expect(errorMessageSpy).toHaveBeenCalledWith(expect.stringContaining("Login failed: No base profile found"));
});
it("should not logout if the base profile cannot be fetched", async () => {
const errorMessageSpy = jest.spyOn(Gui, "errorMessage");
testCache.fetchBaseProfile.mockResolvedValue(null);
await ZoweVsCodeExtension.logoutWithBaseProfile("service");
expect(testCache.fetchBaseProfile).toHaveBeenCalledTimes(1);
expect(testCache.updateBaseProfileFileLogin).not.toHaveBeenCalled();
expect(errorMessageSpy).toHaveBeenCalledWith(expect.stringContaining("Logout failed: No base profile found"));
});

describe("user and password chosen", () => {
@@ -175,7 +179,7 @@ describe("ZoweVsCodeExtension", () => {
it("should logout using the base profile given a simple profile name", async () => {
testCache.fetchBaseProfile.mockResolvedValue(baseProfile);
const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "getServiceProfileForAuthPurposes");
testSpy.mockResolvedValue({ profile: { ...testProfile, ...updProfile } });
testSpy.mockResolvedValue({ ...serviceProfile, profile: { ...testProfile, ...updProfile } });
const logoutSpy = jest.spyOn(Logout, "apimlLogout").mockImplementation(jest.fn());

const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]);
@@ -250,6 +254,51 @@ describe("ZoweVsCodeExtension", () => {
);
quickPickMock.mockRestore();
});
it("should login using the parent profile given a nested profile name", async () => {
const tempBaseProfile = JSON.parse(JSON.stringify(baseProfile));
tempBaseProfile.name = "lpar";
tempBaseProfile.profile.tokenType = "some-dummy-token-type";
testCache.fetchBaseProfile.mockResolvedValue(tempBaseProfile);
const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "getServiceProfileForAuthPurposes");
const newServiceProfile = {
...serviceProfile,
name: "lpar.service",
profile: { ...testProfile, tokenValue: "tokenValue", host: "dummy" },
};
testSpy.mockResolvedValue(newServiceProfile);
jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass").mockResolvedValue(["user", "pass"]);
const loginSpy = jest.spyOn(Login, "apimlLogin").mockResolvedValue("tokenValue");

const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]);
await ZoweVsCodeExtension.loginWithBaseProfile("lpar.service");

const testSession = new Session(JSON.parse(JSON.stringify(expectedSession.ISession)));
delete testSession.ISession.user;
delete testSession.ISession.password;
testSession.ISession.hostname = "dummy";
testSession.ISession.base64EncodedAuth = "dXNlcjpwYXNz";
testSession.ISession.tokenType = tempBaseProfile.profile.tokenType;
testSession.ISession.storeCookie = false;

expect(loginSpy).toHaveBeenCalledWith(testSession);
expect(testSpy).toHaveBeenCalledWith(testCache, "lpar.service");
expect(testCache.updateBaseProfileFileLogin).toHaveBeenCalledWith(
{
name: tempBaseProfile.name,
type: null,
profile: {
...serviceProfile.profile,
tokenType: tempBaseProfile.profile.tokenType,
},
},
{
tokenType: tempBaseProfile.profile.tokenType,
tokenValue: "tokenValue",
},
false
);
quickPickMock.mockRestore();
});
it("should logout using the service profile given a simple profile name", async () => {
testCache.fetchBaseProfile.mockResolvedValue(baseProfile);
const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "getServiceProfileForAuthPurposes");
9 changes: 7 additions & 2 deletions packages/zowe-explorer-api/src/profiles/ProfilesCache.ts
Original file line number Diff line number Diff line change
@@ -316,10 +316,15 @@ export class ProfilesCache {
}

// This will retrieve the base profile from imperative
public async fetchBaseProfile(): Promise<imperative.IProfileLoaded | undefined> {
public async fetchBaseProfile(profileName?: string): Promise<imperative.IProfileLoaded | undefined> {
const mProfileInfo = await this.getProfileInfo();
const baseProfileAttrs = mProfileInfo.getDefaultProfile("base");
if (baseProfileAttrs == null) {
const isUsingTokenAuth = (profName: string): boolean =>
mProfileInfo.getTeamConfig().api.secure.securePropsForProfile(profName).includes("tokenValue");
if ((baseProfileAttrs == null || !isUsingTokenAuth(baseProfileAttrs.profName)) && profileName?.includes(".")) {
const parentProfile = profileName.slice(0, profileName.lastIndexOf("."));
return this.getProfileLoaded(parentProfile, "base", mProfileInfo.getTeamConfig().api.profiles.get(parentProfile));
} else if (baseProfileAttrs == null) {
return undefined;
}
const profAttr = this.getMergedAttrs(mProfileInfo, baseProfileAttrs);
45 changes: 25 additions & 20 deletions packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts
Original file line number Diff line number Diff line change
@@ -120,13 +120,14 @@ export class ZoweVsCodeExtension {
zeProfiles?: ProfilesCache // Profiles extends ProfilesCache
): Promise<boolean> {
const cache: ProfilesCache = zeProfiles ?? ZoweVsCodeExtension.profilesCache;
const baseProfile = await cache.fetchBaseProfile();
if (baseProfile == null) {
return false;
}
if (typeof serviceProfile === "string") {
serviceProfile = await ZoweVsCodeExtension.getServiceProfileForAuthPurposes(cache, serviceProfile);
}
const baseProfile = await cache.fetchBaseProfile(serviceProfile.name);
if (baseProfile == null) {
Gui.errorMessage(`Login failed: No base profile found to store SSO token for profile "${serviceProfile.name}"`);
return false;
}
const tokenType =
serviceProfile.profile.tokenType ?? baseProfile.profile.tokenType ?? loginTokenType ?? imperative.SessConstants.TOKEN_TYPE_APIML;
const updSession = new imperative.Session({
@@ -144,7 +145,10 @@ export class ZoweVsCodeExtension {
{ label: "$(account) User and Password", description: "Log in with basic authentication" },
{ label: "$(note) Certificate", description: "Log in with PEM format certificate file" },
];
const response = await Gui.showQuickPick(qpItems, { placeHolder: "Select an authentication method for obtaining token" });
const response = await Gui.showQuickPick(qpItems, {
placeHolder: "Select an authentication method for obtaining token",
title: `[${baseProfile.name}] Log in to authentication service`,
});
if (response === qpItems[0]) {
const creds = await ZoweVsCodeExtension.promptUserPass({ session: updSession.ISession, rePrompt: true });
if (!creds) {
@@ -180,11 +184,9 @@ export class ZoweVsCodeExtension {
// If base profile already has a token type stored, then we check whether or not the connection details are the same
(serviceProfile.profile.host === baseProfile.profile.host && serviceProfile.profile.port === baseProfile.profile.port);
// If the connection details do not match, then we MUST forcefully store the token in the service profile
let profileToUpdate: imperative.IProfileLoaded;
let profileToUpdate = serviceProfile;
if (connOk) {
profileToUpdate = baseProfile;
} else {
profileToUpdate = serviceProfile;
profileToUpdate = serviceProfile.name.startsWith(baseProfile.name + ".") ? { ...baseProfile, type: null } : baseProfile;
}

await cache.updateBaseProfileFileLogin(profileToUpdate, updBaseProfile, !connOk);
@@ -212,13 +214,13 @@ export class ZoweVsCodeExtension {
serviceProfile: string | imperative.IProfileLoaded,
zeRegister?: Types.IApiRegisterClient, // ZoweExplorerApiRegister
zeProfiles?: ProfilesCache // Profiles extends ProfilesCache
): Promise<void> {
): Promise<boolean> {
const cache: ProfilesCache = zeProfiles ?? ZoweVsCodeExtension.profilesCache;
const baseProfile = await cache.fetchBaseProfile();
if (typeof serviceProfile === "string") {
serviceProfile = await ZoweVsCodeExtension.getServiceProfileForAuthPurposes(cache, serviceProfile);
}
const baseProfile = await cache.fetchBaseProfile(serviceProfile.name);
if (baseProfile) {
if (typeof serviceProfile === "string") {
serviceProfile = await ZoweVsCodeExtension.getServiceProfileForAuthPurposes(cache, serviceProfile);
}
const tokenType =
serviceProfile.profile.tokenType ??
baseProfile.profile.tokenType ??
@@ -234,12 +236,15 @@ export class ZoweVsCodeExtension {
});
await (zeRegister?.getCommonApi(serviceProfile).logout ?? Logout.apimlLogout)(updSession);

const connOk = serviceProfile.profile.host === baseProfile.profile.host && serviceProfile.profile.port === baseProfile.profile.port;
if (connOk) {
await cache.updateBaseProfileFileLogout(baseProfile);
} else {
await cache.updateBaseProfileFileLogout(serviceProfile);
}
const connOk =
serviceProfile.profile.host === baseProfile.profile.host &&
serviceProfile.profile.port === baseProfile.profile.port &&
!serviceProfile.name.startsWith(baseProfile.name + ".");
await cache.updateBaseProfileFileLogout(connOk ? baseProfile : serviceProfile);
return true;
} else {
Gui.errorMessage(`Logout failed: No base profile found to store SSO token for profile "${serviceProfile.name}"`);
return false;
}
}

1 change: 1 addition & 0 deletions packages/zowe-explorer/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -44,6 +44,7 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen
- Implemented the `onVaultUpdate` VSCode events to notify extenders when credentials are updated on the OS vault by other applications. [#2994](https://github.com/zowe/zowe-explorer-vscode/pull/2994)
- Changed default base profile naming scheme in newly generated configuration files to prevent name and property conflicts between Global and Project profiles [#2682](https://github.com/zowe/zowe-explorer-vscode/issues/2682)
- Implemented the `onCredMgrUpdate` VSCode events to notify extenders when the local PC's credential manager has been updated by other applications. [#2994](https://github.com/zowe/zowe-explorer-vscode/pull/2994)
- Added support for logging in to multiple API ML instances per team config file. [#2264](https://github.com/zowe/zowe-explorer-vscode/issues/2264)

### Bug fixes

Original file line number Diff line number Diff line change
@@ -410,7 +410,14 @@ export function createInstanceOfProfileInfo() {
updateProperty: jest.fn(),
updateKnownProperty: jest.fn(),
createSession: jest.fn(),
getTeamConfig: () => ({ exists: true }),
getTeamConfig: () => ({
api: {
secure: {
securePropsForProfile: jest.fn().mockReturnValue([]),
},
},
exists: true,
}),
mergeArgsForProfile: jest.fn().mockReturnValue({
knownArgs: [
{
Original file line number Diff line number Diff line change
@@ -1136,7 +1136,7 @@ describe("Profiles Unit Tests - function ssoLogout", () => {
globalMocks.testSession,
globalMocks.testProfile
);

testNode.profile.profile.password = undefined;
testNode.profile.profile.user = "fake";
Object.defineProperty(Profiles.getInstance(), "allProfiles", {
value: [
21 changes: 12 additions & 9 deletions packages/zowe-explorer/src/configuration/Profiles.ts
Original file line number Diff line number Diff line change
@@ -835,6 +835,7 @@ export class Profiles extends ProfilesCache {

try {
this.clearFilterFromAllTrees(node);
let logoutOk = true;

// this will handle extenders
if (
@@ -844,16 +845,18 @@ export class Profiles extends ProfilesCache {
) {
await ZoweExplorerApiRegister.getInstance().getCommonApi(serviceProfile).logout(node.getSession());
} else {
await ZoweVsCodeExtension.logoutWithBaseProfile(serviceProfile, ZoweExplorerApiRegister.getInstance(), this);
logoutOk = await ZoweVsCodeExtension.logoutWithBaseProfile(serviceProfile, ZoweExplorerApiRegister.getInstance(), this);
}
if (logoutOk) {
Gui.showMessage(
vscode.l10n.t({
message: "Logout from authentication service was successful for {0}.",
args: [serviceProfile.name],
comment: ["Service profile name"],
})
);
await Profiles.getInstance().refresh(ZoweExplorerApiRegister.getInstance());
}
Gui.showMessage(
vscode.l10n.t({
message: "Logout from authentication service was successful for {0}.",
args: [serviceProfile.name],
comment: ["Service profile name"],
})
);
await Profiles.getInstance().refresh(ZoweExplorerApiRegister.getInstance());
} catch (error) {
const message = vscode.l10n.t({
message: "Unable to log out with {0}. {1}",