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

ZE API for direct connect login/logout & adoption in ZE processes #3459

Merged
merged 11 commits into from
Feb 18, 2025
1 change: 1 addition & 0 deletions packages/zowe-explorer-api/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t
### New features and enhancements

- Added new `copyDataSetCrossLpar` API to provide ability to copy/paste data sets across LPARs. [#3012](https://github.com/zowe/zowe-explorer-vscode/issues/3012)
- Added new `directConnectLogin` and `directConnectLogout` to the ZoweVsCodeExtension class. [#3346](https://github.com/zowe/zowe-explorer-vscode/issues/3346)

### Bug fixes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -910,4 +910,71 @@ describe("ZoweVsCodeExtension", () => {
expect(options.session.certKey).toEqual("/test/key/path");
});
});
describe("Direct connect token authentication methods", () => {
let blockMocks: ReturnType<typeof createBlockMocks>;
const expectedSession = new imperative.Session({
hostname: "dummy",
password: "Password",
port: 1234,
tokenType: "jwtToken",
type: "token",
user: "Username",
});

function createBlockMocks() {
const vals = {
serviceProfile: {
failNotFound: false,
message: "",
name: "service",
type: "service",
profile: {
host: "dummy",
port: 1234,
},
},
promptSpy: jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass"),
testNode: undefined,
testRegister: {
getCommonApi: () => ({
login: jest.fn().mockReturnValue("tokenValue"),
logout: jest.fn(),
getSession: jest.fn().mockReturnValue(new imperative.Session(JSON.parse(JSON.stringify(expectedSession.ISession)))),
}),
},
};
jest.spyOn(ZoweVsCodeExtension as any, "profilesCache", "get").mockReturnValue({
updateCachedProfile: jest.fn(),
});
vals.testNode = {
getSession: jest.fn().mockReturnValue(new imperative.Session(JSON.parse(JSON.stringify(expectedSession.ISession)))),
} as any;
return vals;
}
beforeEach(() => {
blockMocks = createBlockMocks();
});
it("directConnectLogin should obtain a JWT and return true using profile to get session", async () => {
blockMocks.promptSpy.mockResolvedValue(["user", "pass"]);
expect(await (ZoweVsCodeExtension as any).directConnectLogin(blockMocks.serviceProfile, blockMocks.testRegister)).toEqual(true);
});
it("directConnectLogin should obtain a JWT and return true using node to get session", async () => {
blockMocks.promptSpy.mockResolvedValue(["user", "pass"]);
expect(
await (ZoweVsCodeExtension as any).directConnectLogin(blockMocks.serviceProfile, blockMocks.testRegister, blockMocks.testNode)
).toEqual(true);
});
it("directConnectLogin should not obtain a JWT and return false due to no credentials entered using profile to get session", async () => {
blockMocks.promptSpy.mockResolvedValue(undefined);
expect(await (ZoweVsCodeExtension as any).directConnectLogin(blockMocks.serviceProfile, blockMocks.testRegister)).toEqual(false);
});
it("directConnectLogout should retire JWT and return true using profile to get session", async () => {
expect(await (ZoweVsCodeExtension as any).directConnectLogout(blockMocks.serviceProfile, blockMocks.testRegister)).toEqual(true);
});
it("directConnectLogout should retire JWT and return true using node to get session", async () => {
expect(
await (ZoweVsCodeExtension as any).directConnectLogout(blockMocks.serviceProfile, blockMocks.testRegister, blockMocks.testNode)
).toEqual(true);
});
});
});
1 change: 0 additions & 1 deletion packages/zowe-explorer-api/src/profiles/AuthHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { CorrelatedError, FileManagement } from "../utils";
import * as imperative from "@zowe/imperative";
import { IZoweTreeNode } from "../tree";
import { Mutex } from "async-mutex";
import { ZoweVsCodeExtension } from "../vscode/ZoweVsCodeExtension";

/**
* @brief individual authentication methods (also supports a `ProfilesCache` class)
Expand Down
82 changes: 73 additions & 9 deletions packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,7 @@ export class ZoweVsCodeExtension {
*/
public static async ssoLogin(opts: BaseProfileAuthOptions): Promise<boolean> {
const cache: ProfilesCache = opts.zeProfiles ?? ZoweVsCodeExtension.profilesCache;
const serviceProfile =
typeof opts.serviceProfile === "string"
? await ZoweVsCodeExtension.getServiceProfileForAuthPurposes(cache, opts.serviceProfile)
: opts.serviceProfile;
const serviceProfile = await this.getServiceProfile(opts);
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}"`);
Expand Down Expand Up @@ -223,6 +220,37 @@ export class ZoweVsCodeExtension {
return true;
}

/**
* Trigger a direct connection login process, not via API ML
*
* @param {imperative.IProfileLoaded} [serviceProfile] Instance of profile to be used for obtaining token
* @param {Types.IApiRegisterClient} [zeRegister] Instance of `IApiRegisterClient`
* @param {Types.IZoweNodeType} [node] Optional instance of `IZoweNodeType`
* @returns boolean value of successful login
*/
public static async directConnectLogin(
serviceProfile: imperative.IProfileLoaded,
zeRegister: Types.IApiRegisterClient,
node?: Types.IZoweNodeType
): Promise<boolean> {
const zeCommon = zeRegister.getCommonApi(serviceProfile);
let session: imperative.Session;
if (node) {
session = node.getSession();
} else {
session = zeCommon?.getSession();
}
const creds = await this.promptUserPass({ session: session.ISession, rePrompt: true });
if (!creds) {
return false;
}
session.ISession.user = creds[0];
session.ISession.password = creds[1];
await zeCommon?.login(session);
await this.profilesCache.updateCachedProfile(serviceProfile, node);
return true;
}

/**
* Trigger a logout operation with the merged contents between the service profile and the base profile.
* If the connection details (host:port) do not match (service vs base), the token will be removed from the service profile.
Expand All @@ -248,11 +276,7 @@ export class ZoweVsCodeExtension {
*/
public static async ssoLogout(opts: BaseProfileAuthOptions): Promise<boolean> {
const cache: ProfilesCache = opts.zeProfiles ?? ZoweVsCodeExtension.profilesCache;
const serviceProfile =
typeof opts.serviceProfile === "string"
? await ZoweVsCodeExtension.getServiceProfileForAuthPurposes(cache, opts.serviceProfile)
: opts.serviceProfile;

const serviceProfile = await this.getServiceProfile(opts);
const baseProfile = await cache.fetchBaseProfile(serviceProfile.name);
if (!baseProfile) {
Gui.errorMessage(`Logout failed: No base profile found to remove SSO token for profile "${serviceProfile.name}"`);
Expand Down Expand Up @@ -286,6 +310,46 @@ export class ZoweVsCodeExtension {
return true;
}

/**
* Trigger a direct connection logout process, not via API ML
*
* @param {imperative.IProfileLoaded} [serviceProfile] Instance of profile to be used for retiring token
* @param {Types.IApiRegisterClient} [zeRegister] Instance of `IApiRegisterClient`
* @param {Types.IZoweNodeType} [node] Optional instance of `IZoweNodeType`
* @returns boolean value of successful logout
*/
public static async directConnectLogout(
serviceProfile: imperative.IProfileLoaded,
zeRegister: Types.IApiRegisterClient,
node?: Types.IZoweNodeType
): Promise<boolean> {
const zeCommon = zeRegister.getCommonApi(serviceProfile);
let session: imperative.Session;
if (node) {
session = node.getSession();
} else {
session = zeCommon?.getSession();
}
await zeCommon?.logout(session);
await this.profilesCache.updateCachedProfile(serviceProfile, node);
return true;
}

/**
* This method is intended to be used for authentication (login, logout) purposes
*
* Note: this method calls the `getServiceProfileForAuthPurposes()` which creates a new instance of the ProfileInfo APIs
* Be aware that any non-saved updates will not be considered here.
* @param {BaseProfileAuthOptions} opts Object defining options for base profile authentication
* @returns The IProfileLoaded with tokenType and tokenValue
*/
private static async getServiceProfile(opts: BaseProfileAuthOptions): Promise<imperative.IProfileLoaded> {
const cache: ProfilesCache = opts.zeProfiles ?? ZoweVsCodeExtension.profilesCache;
return typeof opts.serviceProfile === "string"
? await ZoweVsCodeExtension.getServiceProfileForAuthPurposes(cache, opts.serviceProfile)
: opts.serviceProfile;
}

/**
* This method is intended to be used for authentication (login, logout) purposes
*
Expand Down
1 change: 1 addition & 0 deletions packages/zowe-explorer/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen
- Added Time Started, Time Ended, and Time Submitted job properties to the Jobs table view. [#3055](https://github.com/zowe/zowe-explorer-vscode/issues/3055)
- Implemented copy/paste functionality of data sets within and across LPARs. [#3012](https://github.com/zowe/zowe-explorer-vscode/issues/3012)
- Implemented drag and drop functionality of data sets within and across LPARs. [#3413](https://github.com/zowe/zowe-explorer-vscode/pull/3413)
- Adopted ZE APIs new `directConnectLogin` and `directConnectLogout` methods for login and logout actions NOT using the tokenType `apimlAuthenticationToken`. [#3346](https://github.com/zowe/zowe-explorer-vscode/issues/3346)

### Bug fixes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1229,7 +1229,6 @@ describe("Profiles Unit Tests - function ssoLogin", () => {
getTokenTypeName: () => imperative.SessConstants.TOKEN_TYPE_APIML,
login: () => "ajshdlfkjshdalfjhas",
} as never);
jest.spyOn(Profiles.getInstance() as any, "loginCredentialPrompt").mockReturnValue(["fake", "12345"]);
jest.spyOn(Profiles.getInstance() as any, "updateBaseProfileFileLogin").mockImplementation();
await expect(Profiles.getInstance().ssoLogin(testNode, "fake")).resolves.not.toThrow();
});
Expand All @@ -1239,7 +1238,6 @@ describe("Profiles Unit Tests - function ssoLogin", () => {
login: () => "ajshdlfkjshdalfjhas",
getSession: () => globalMocks.testSession,
} as never);
jest.spyOn(Profiles.getInstance() as any, "loginCredentialPrompt").mockReturnValue(["fake", "12345"]);
await expect(Profiles.getInstance().ssoLogin(testNode, "fake")).resolves.not.toThrow();
});
it("should catch error getting token type and log warning", async () => {
Expand All @@ -1258,7 +1256,6 @@ describe("Profiles Unit Tests - function ssoLogin", () => {
login: jest.fn(),
} as never);
const loginBaseProfMock = jest.spyOn(ZoweVsCodeExtension, "ssoLogin").mockRejectedValueOnce(new Error("test error."));
jest.spyOn(Profiles.getInstance() as any, "loginCredentialPrompt").mockReturnValue(["fake", "12345"]);
await expect(Profiles.getInstance().ssoLogin(testNode, "fake")).resolves.not.toThrow();
expect(ZoweLogger.error).toHaveBeenCalled();
loginBaseProfMock.mockRestore();
Expand All @@ -1271,7 +1268,6 @@ describe("Profiles Unit Tests - function ssoLogin", () => {
const profileUpdatedEmitterSpy = jest.spyOn(ZoweVsCodeExtension.onProfileUpdatedEmitter, "fire");
const unlockProfileSpy = jest.spyOn(AuthHandler, "unlockProfile");
const loginBaseProfMock = jest.spyOn(ZoweVsCodeExtension, "ssoLogin").mockResolvedValueOnce(true);
jest.spyOn(Profiles.getInstance() as any, "loginCredentialPrompt").mockReturnValue(["fake", "12345"]);
await expect(Profiles.getInstance().ssoLogin(testNode, "fake")).resolves.not.toThrow();
expect(profileUpdatedEmitterSpy).toHaveBeenCalledTimes(1);
expect(profileUpdatedEmitterSpy).toHaveBeenCalledWith(globalMocks.testProfile);
Expand Down Expand Up @@ -1504,7 +1500,7 @@ describe("Profiles Unit Tests - function handleSwitchAuthentication", () => {
jest.spyOn(ZoweExplorerApiRegister.getInstance(), "getCommonApi").mockReturnValue({
getTokenTypeName: () => "jwtToken",
} as never);
jest.spyOn(Profiles.getInstance() as any, "loginWithRegularProfile").mockResolvedValue(true);
jest.spyOn(ZoweVsCodeExtension as any, "directConnectLogin").mockResolvedValue(true);
await Profiles.getInstance().handleSwitchAuthentication(testNode);
expect(Gui.showMessage).toHaveBeenCalled();
expect(testNode.profile.profile.tokenType).toBe(modifiedTestNode.profile.profile.tokenType);
Expand Down Expand Up @@ -1562,7 +1558,6 @@ describe("Profiles Unit Tests - function handleSwitchAuthentication", () => {
jest.spyOn(ZoweExplorerApiRegister.getInstance(), "getCommonApi").mockReturnValue({
getTokenTypeName: () => "jwtToken",
} as never);
jest.spyOn(Profiles.getInstance() as any, "loginWithRegularProfile").mockResolvedValue(false);
await Profiles.getInstance().handleSwitchAuthentication(testNode);
expect(Gui.errorMessage).toHaveBeenCalled();
expect(testNode.profile.profile.tokenType).toBe(modifiedTestNode.profile.profile.tokenType);
Expand Down Expand Up @@ -1977,36 +1972,6 @@ describe("Profiles Unit Tests - function validationArraySetup", () => {
});
});

describe("Profiles Unit Tests - function loginCredentialPrompt", () => {
afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
});

it("should show a gui message if there is not a newUser", async () => {
const privateProfile = Profiles.getInstance() as any;
Object.defineProperty(privateProfile, "userInfo", {
value: () => null,
});
const showMessageSpy = jest.spyOn(Gui, "showMessage").mockImplementation();
await expect(privateProfile.loginCredentialPrompt()).resolves.toEqual(undefined);
expect(showMessageSpy).toHaveBeenCalledTimes(1);
});

it("should show a gui message if there is not a newUser", async () => {
const privateProfile = Profiles.getInstance() as any;
Object.defineProperty(Profiles, "getInstance", {
value: () => ({
userInfo: () => "test",
passwordInfo: () => null,
}),
});
const showMessageSpy = jest.spyOn(Gui, "showMessage").mockImplementation();
await expect(privateProfile.loginCredentialPrompt()).resolves.toEqual(undefined);
expect(showMessageSpy).toHaveBeenCalledTimes(1);
});
});

describe("Profiles Unit Tests - function getSecurePropsForProfile", () => {
afterEach(() => {
jest.clearAllMocks();
Expand Down
2 changes: 0 additions & 2 deletions packages/zowe-explorer/l10n/bundle.l10n.json
Original file line number Diff line number Diff line change
Expand Up @@ -1098,8 +1098,6 @@
"Select the location of the config file to edit": "Select the location of the config file to edit",
"Global: in the Zowe home directory": "Global: in the Zowe home directory",
"Project: in the current working directory": "Project: in the current working directory",
"Enter the user name for the connection. Leave blank to not store.": "Enter the user name for the connection. Leave blank to not store.",
"Enter the password for the connection. Leave blank to not store.": "Enter the password for the connection. Leave blank to not store.",
"Do you wish to apply this for all trees?": "Do you wish to apply this for all trees?",
"Apply to all trees": "Apply to all trees",
"Apply to current tree selected": "Apply to current tree selected",
Expand Down
2 changes: 0 additions & 2 deletions packages/zowe-explorer/l10n/poeditor.json
Original file line number Diff line number Diff line change
Expand Up @@ -901,8 +901,6 @@
"Select the location of the config file to edit": "",
"Global: in the Zowe home directory": "",
"Project: in the current working directory": "",
"Enter the user name for the connection. Leave blank to not store.": "",
"Enter the password for the connection. Leave blank to not store.": "",
"Do you wish to apply this for all trees?": "",
"Apply to all trees": "",
"Apply to current tree selected": "",
Expand Down
Loading
Loading