diff --git a/packages/zowe-explorer-api/CHANGELOG.md b/packages/zowe-explorer-api/CHANGELOG.md index ba71e6990b..c9cd9c52c0 100644 --- a/packages/zowe-explorer-api/CHANGELOG.md +++ b/packages/zowe-explorer-api/CHANGELOG.md @@ -6,9 +6,13 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t ### New features and enhancements +- Added the `BaseProfileAuthOptions` interface to define base profile authentication options for SSO login and logout. [#3076](https://github.com/zowe/zowe-explorer-vscode/pull/3076) +- Deprecated the methods `ZoweVsCodeExtension.loginWithBaseProfile` and `ZoweVsCodeExtension.logoutWithBaseProfile`. Use `ZoweVsCodeExtension.ssoLogin` and `ZoweVsCodeExtension.ssoLogout` instead, which use the `BaseProfileAuthOptions` interface and allow you to choose whether the token value in the base profile should have precedence in case there are conflicts. [#3076](https://github.com/zowe/zowe-explorer-vscode/pull/3076) + ### Bug fixes - Updated the `TableViewProvider.setTableView` function to show the Zowe Resources panel if a table is provided. If `null` is passed, the Zowe Resources panel will be hidden. [#3113](https://github.com/zowe/zowe-explorer-vscode/issues/3113) +- Fixed behavior of logout action when token is defined in both base profile and parent profile. [#3076](https://github.com/zowe/zowe-explorer-vscode/issues/3076) ## `3.0.0-next.202409132122` @@ -128,6 +132,7 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t - **Breaking:** Consolidated WebView API options into a single object (`WebViewOpts` type), both for developer convenience and to support future options. - Enhanced the `ZoweVsCodeExtension.loginWithBaseProfile` and `ZoweVsCodeExtension.logoutWithBaseProfile` methods to store SSO token in parent profile when nested profiles are in use. [#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. +- **Breaking:** Changed behavior of the `ProfilesCache.fetchBaseProfile` method so that if a nested profile name is specified (e.g. "lpar.zosmf"), then its parent profile is returned unless token is already stored in the base profile. - Renamed the `_lookup` function to `lookup` in the `BaseProvider` class and updated its access to public, allowing extenders to look up resources outside of the provider implementations. The `_lookup` function is still accessible, but now deprecated in favor of the public `lookup` function. [#3040](https://github.com/zowe/zowe-explorer-vscode/pull/3040) - **Breaking:** Removed the `MemberEntry` filesystem class, in favor of using the `DsEntry` class with `isMember` set to `true`. - Changed `TableViewProvider.setTableView` function to be asynchronous for more optimized data updates. diff --git a/packages/zowe-explorer-api/__tests__/__unit__/profiles/ProfilesCache.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/profiles/ProfilesCache.unit.test.ts index b1f80ce4dd..90b0e35e2e 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/profiles/ProfilesCache.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/profiles/ProfilesCache.unit.test.ts @@ -87,12 +87,11 @@ function createProfInfoMock(profiles: Partial[]): imp const teamConfigApi: Partial = { api: { profiles: { - get: jest.fn(), + get: jest.fn().mockReturnValue({}), getProfilePathFromName: jest.fn().mockImplementation((x) => x), }, secure: { secureFields: jest.fn().mockReturnValue([]), - securePropsForProfile: jest.fn().mockReturnValue([]), }, } as any, exists: true, @@ -553,18 +552,18 @@ describe("ProfilesCache", () => { expect(profile).toMatchObject({ name: "lpar1", type: "base" }); }); - it("fetchBaseProfile should return typeless profile if base profile does not contain token value", async () => { + it("fetchBaseProfile should return typeless profile if base profile does not contain token type", 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 () => { + it("fetchBaseProfile should return base profile if it contains token type", 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"]); + mocked(profInfoMock.getTeamConfig().api.profiles.get).mockReturnValueOnce({ tokenType: imperative.SessConstants.TOKEN_TYPE_JWT }); const profile = await profCache.fetchBaseProfile("lpar1.zosmf"); expect(profile).toMatchObject(baseProfile); }); diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ZoweVsCodeExtension.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ZoweVsCodeExtension.unit.test.ts index 20cdc6bd72..e3c431880e 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ZoweVsCodeExtension.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ZoweVsCodeExtension.unit.test.ts @@ -11,15 +11,14 @@ import * as vscode from "vscode"; import { Gui } from "../../../src/globals/Gui"; -import { Session } from "@zowe/imperative"; -import { PromptCredentialsOptions, ZoweVsCodeExtension } from "../../../src/vscode"; -import { ProfilesCache, Types } from "../../../src"; +import { PromptCredentialsOptions, ZoweVsCodeExtension, ProfilesCache, Types } from "../../../src"; import { Login, Logout } from "@zowe/core-for-zowe-sdk"; import * as imperative from "@zowe/imperative"; describe("ZoweVsCodeExtension", () => { + const fakeLogger = { debug: jest.fn() }; const fakeVsce = { - exports: "zowe", + exports: undefined, packageJSON: { version: "1.0.1" }, } as vscode.Extension; @@ -90,126 +89,179 @@ describe("ZoweVsCodeExtension", () => { }); describe("login and logout with base profiles", () => { - const testProfile = { - host: "dummy", - port: 1234, - }; - const baseProfile = { name: "base", type: "base", profile: testProfile }; - const serviceProfile: any = { name: "service", type: "service", profile: testProfile }; - const allProfiles = [serviceProfile, baseProfile]; - const testNode: any = { - setProfileToChoice: jest.fn(), - getProfile: jest.fn().mockReturnValue(serviceProfile), - }; - const expectedSession = new Session({ - hostname: "dummy", - password: "Password", - port: 1234, - tokenType: "apimlAuthenticationToken", - type: "token", - user: "Username", - }); - const updProfile = { tokenType: "apimlAuthenticationToken", tokenValue: "tokenValue" }; - const testRegister: any = { - getCommonApi: () => ({ - login: jest.fn().mockReturnValue("tokenValue"), - logout: jest.fn(), - getTokenTypeName: () => "apimlAuthenticationToken", - }), - }; - const testCache: any = { - allProfiles, - allExternalTypes: [], - fetchBaseProfile: jest.fn(), - loadNamedProfile: jest.fn().mockReturnValue(serviceProfile), - updateBaseProfileFileLogin: jest.fn(), - updateBaseProfileFileLogout: jest.fn(), - getLoadedProfConfig: jest.fn().mockReturnValue({ profile: {} }), - getProfileInfo: jest.fn().mockReturnValue({ - isSecured: jest.fn().mockReturnValue(false), - getAllProfiles: jest.fn().mockReturnValue(allProfiles), - mergeArgsForProfile: jest.fn().mockReturnValue({ knownArgs: [] }), - }), - refresh: jest.fn(), - }; + let blockMocks: ReturnType; + + function createBlockMocks() { + const testProfile: imperative.IProfile = { + host: "dummy", + port: 1234, + }; + const baseProfile: imperative.IProfileLoaded = { + failNotFound: false, + message: "", + name: "base", + type: "base", + profile: { ...testProfile }, + }; + const serviceProfile: imperative.IProfileLoaded = { + failNotFound: false, + message: "", + name: "service", + type: "service", + profile: { ...testProfile }, + }; + const testCache = new ProfilesCache(fakeLogger as unknown as imperative.Logger); + const testProfInfo = new imperative.ProfileInfo("zowe"); + const configLayer: imperative.IConfigLayer = { + exists: true, + path: "zowe.config.json", + properties: { + ...imperative.Config.empty(), + profiles: { + service: { + type: "service", + properties: testProfile, + }, + base: { + type: "base", + properties: testProfile, + }, + }, + }, + global: false, + user: true, + }; + const testConfig = new (imperative.Config as any)(); + Object.assign(testConfig, { + ...new (imperative.Config as any)(), + layerActive: jest.fn().mockReturnValue(configLayer), + layerMerge: jest.fn().mockReturnValue(configLayer.properties), + mLayers: [configLayer], + }); + Object.assign(testProfInfo, { + getTeamConfig: jest.fn().mockReturnValue(testConfig), + loadSchema: jest.fn().mockReturnValue({}), + mLoadedConfig: testConfig, + mProfileSchemaCache: new Map(), + readProfilesFromDisk: jest.fn(), + }); + jest.spyOn(testCache, "getProfileInfo").mockResolvedValue(testProfInfo); + + return { + testProfile, + baseProfile, + serviceProfile, + testNode: { + setProfileToChoice: jest.fn(), + getProfile: jest.fn().mockReturnValue(serviceProfile), + }, + expectedSession: new imperative.Session({ + hostname: "dummy", + password: "Password", + port: 1234, + tokenType: "apimlAuthenticationToken", + type: "token", + user: "Username", + }), + updProfile: { tokenType: "apimlAuthenticationToken", tokenValue: "tokenValue" }, + testRegister: { + getCommonApi: () => ({ + login: jest.fn().mockReturnValue("tokenValue"), + logout: jest.fn(), + getTokenTypeName: () => "apimlAuthenticationToken", + }), + }, + configLayer, + testCache, + }; + } beforeEach(() => { - jest.spyOn(ZoweVsCodeExtension as any, "profilesCache", "get").mockReturnValue(testCache); + blockMocks = createBlockMocks(); + jest.spyOn(ZoweVsCodeExtension as any, "profilesCache", "get").mockReturnValue(blockMocks.testCache); jest.spyOn(vscode.extensions, "getExtension").mockReturnValueOnce(fakeVsce); }); 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(); + const fetchBaseProfileSpy = jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(undefined); + const updateBaseProfileFileLoginSpy = jest.spyOn(blockMocks.testCache, "updateBaseProfileFileLogin"); + await ZoweVsCodeExtension.ssoLogin({ serviceProfile: "service" }); + expect(fetchBaseProfileSpy).toHaveBeenCalledTimes(1); + expect(updateBaseProfileFileLoginSpy).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(); + const fetchBaseProfileSpy = jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(undefined); + const updateBaseProfileFileLoginSpy = jest.spyOn(blockMocks.testCache, "updateBaseProfileFileLogin"); + await ZoweVsCodeExtension.ssoLogout({ serviceProfile: "service" }); + expect(fetchBaseProfileSpy).toHaveBeenCalledTimes(1); + expect(updateBaseProfileFileLoginSpy).not.toHaveBeenCalled(); expect(errorMessageSpy).toHaveBeenCalledWith(expect.stringContaining("Logout failed: No base profile found")); }); describe("user and password chosen", () => { it("should login using the base profile given a simple profile name", async () => { - testCache.fetchBaseProfile.mockResolvedValue(baseProfile); + jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(blockMocks.baseProfile); + const updateBaseProfileFileLoginSpy = jest.spyOn(blockMocks.testCache, "updateBaseProfileFileLogin"); const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "getServiceProfileForAuthPurposes"); 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("service"); + await ZoweVsCodeExtension.ssoLogin({ serviceProfile: "service" }); - const testSession = new Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + const testSession = new imperative.Session(JSON.parse(JSON.stringify(blockMocks.expectedSession.ISession))); delete testSession.ISession.user; delete testSession.ISession.password; testSession.ISession.base64EncodedAuth = "dXNlcjpwYXNz"; testSession.ISession.storeCookie = false; expect(loginSpy).toHaveBeenCalledWith(testSession); - expect(testSpy).toHaveBeenCalledWith(testCache, "service"); + expect(testSpy).toHaveBeenCalledWith(blockMocks.testCache, "service"); expect(quickPickMock).toHaveBeenCalled(); - expect(testCache.updateBaseProfileFileLogin).toHaveBeenCalledWith(baseProfile, updProfile, false); + expect(updateBaseProfileFileLoginSpy).toHaveBeenCalledWith(blockMocks.baseProfile, blockMocks.updProfile, false); }); it("should logout using the base profile given a simple profile name", async () => { - testCache.fetchBaseProfile.mockResolvedValue(baseProfile); + jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(blockMocks.baseProfile); + const updateBaseProfileFileLogoutSpy = jest.spyOn(blockMocks.testCache, "updateBaseProfileFileLogout"); const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "getServiceProfileForAuthPurposes"); - testSpy.mockResolvedValue({ ...serviceProfile, profile: { ...testProfile, ...updProfile } }); + testSpy.mockResolvedValue({ ...blockMocks.serviceProfile, profile: { ...blockMocks.testProfile, ...blockMocks.updProfile } }); const logoutSpy = jest.spyOn(Logout, "apimlLogout").mockImplementation(jest.fn()); const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); - await ZoweVsCodeExtension.logoutWithBaseProfile("service"); + await ZoweVsCodeExtension.ssoLogout({ serviceProfile: "service" }); - const testSession = new Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + const testSession = new imperative.Session(JSON.parse(JSON.stringify(blockMocks.expectedSession.ISession))); testSession.ISession.tokenValue = "tokenValue"; delete testSession.ISession.base64EncodedAuth; delete testSession.ISession.user; delete testSession.ISession.password; expect(logoutSpy).toHaveBeenCalledWith(testSession); - expect(testSpy).toHaveBeenCalledWith(testCache, "service"); - expect(testCache.updateBaseProfileFileLogout).toHaveBeenCalledWith(baseProfile); + expect(testSpy).toHaveBeenCalledWith(blockMocks.testCache, "service"); + expect(updateBaseProfileFileLogoutSpy).toHaveBeenCalledWith(blockMocks.baseProfile); quickPickMock.mockRestore(); }); it("should login using the base profile if the base profile does not have a tokenType stored", async () => { - const tempBaseProfile = JSON.parse(JSON.stringify(baseProfile)); + const tempBaseProfile = JSON.parse(JSON.stringify(blockMocks.baseProfile)); tempBaseProfile.profile.tokenType = undefined; - testCache.fetchBaseProfile.mockResolvedValue(tempBaseProfile); + jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(tempBaseProfile); + const updateBaseProfileFileLoginSpy = jest.spyOn(blockMocks.testCache, "updateBaseProfileFileLogin"); const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "getServiceProfileForAuthPurposes"); - const newServiceProfile = { ...serviceProfile, profile: { ...testProfile, tokenValue: "tokenValue", host: "service" } }; + const newServiceProfile = { + ...blockMocks.serviceProfile, + profile: { ...blockMocks.testProfile, tokenValue: "tokenValue", host: "service" }, + }; 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("service"); + await ZoweVsCodeExtension.ssoLogin({ serviceProfile: "service" }); - const testSession = new Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + const testSession = new imperative.Session(JSON.parse(JSON.stringify(blockMocks.expectedSession.ISession))); delete testSession.ISession.user; delete testSession.ISession.password; testSession.ISession.hostname = "service"; @@ -217,24 +269,28 @@ describe("ZoweVsCodeExtension", () => { testSession.ISession.storeCookie = false; expect(loginSpy).toHaveBeenCalledWith(testSession); - expect(testSpy).toHaveBeenCalledWith(testCache, "service"); - expect(testCache.updateBaseProfileFileLogin).toHaveBeenCalledWith(tempBaseProfile, updProfile, false); + expect(testSpy).toHaveBeenCalledWith(blockMocks.testCache, "service"); + expect(updateBaseProfileFileLoginSpy).toHaveBeenCalledWith(tempBaseProfile, blockMocks.updProfile, false); quickPickMock.mockRestore(); }); it("should login using the service profile given a simple profile name", async () => { - const tempBaseProfile = JSON.parse(JSON.stringify(baseProfile)); + const tempBaseProfile = JSON.parse(JSON.stringify(blockMocks.baseProfile)); tempBaseProfile.profile.tokenType = "some-dummy-token-type"; - testCache.fetchBaseProfile.mockResolvedValue(tempBaseProfile); + jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(tempBaseProfile); + const updateBaseProfileFileLoginSpy = jest.spyOn(blockMocks.testCache, "updateBaseProfileFileLogin"); const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "getServiceProfileForAuthPurposes"); - const newServiceProfile = { ...serviceProfile, profile: { ...testProfile, tokenValue: "tokenValue", host: "service" } }; + const newServiceProfile = { + ...blockMocks.serviceProfile, + profile: { ...blockMocks.testProfile, tokenValue: "tokenValue", host: "service" }, + }; 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("service"); + await ZoweVsCodeExtension.ssoLogin({ serviceProfile: "service" }); - const testSession = new Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + const testSession = new imperative.Session(JSON.parse(JSON.stringify(blockMocks.expectedSession.ISession))); delete testSession.ISession.user; delete testSession.ISession.password; testSession.ISession.hostname = "service"; @@ -243,8 +299,8 @@ describe("ZoweVsCodeExtension", () => { testSession.ISession.storeCookie = false; expect(loginSpy).toHaveBeenCalledWith(testSession); - expect(testSpy).toHaveBeenCalledWith(testCache, "service"); - expect(testCache.updateBaseProfileFileLogin).toHaveBeenCalledWith( + expect(testSpy).toHaveBeenCalledWith(blockMocks.testCache, "service"); + expect(updateBaseProfileFileLoginSpy).toHaveBeenCalledWith( newServiceProfile, { tokenType: tempBaseProfile.profile.tokenType, @@ -255,24 +311,36 @@ describe("ZoweVsCodeExtension", () => { quickPickMock.mockRestore(); }); it("should login using the parent profile given a nested profile name", async () => { - const tempBaseProfile = JSON.parse(JSON.stringify(baseProfile)); + const tempBaseProfile = JSON.parse(JSON.stringify(blockMocks.baseProfile)); tempBaseProfile.name = "lpar"; tempBaseProfile.profile.tokenType = "some-dummy-token-type"; - testCache.fetchBaseProfile.mockResolvedValue(tempBaseProfile); + jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(tempBaseProfile); + const updateBaseProfileFileLoginSpy = jest.spyOn(blockMocks.testCache, "updateBaseProfileFileLogin"); const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "getServiceProfileForAuthPurposes"); const newServiceProfile = { - ...serviceProfile, + ...blockMocks.serviceProfile, name: "lpar.service", - profile: { ...testProfile, tokenValue: "tokenValue", host: "dummy" }, + profile: { ...blockMocks.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"); + blockMocks.configLayer.properties.profiles = { + lpar: { + profiles: { + service: { + type: "service", + properties: [], + }, + }, + properties: [], + }, + }; const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); - await ZoweVsCodeExtension.loginWithBaseProfile("lpar.service"); + await ZoweVsCodeExtension.ssoLogin({ serviceProfile: "lpar.service" }); - const testSession = new Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + const testSession = new imperative.Session(JSON.parse(JSON.stringify(blockMocks.expectedSession.ISession))); delete testSession.ISession.user; delete testSession.ISession.password; testSession.ISession.hostname = "dummy"; @@ -281,13 +349,13 @@ describe("ZoweVsCodeExtension", () => { testSession.ISession.storeCookie = false; expect(loginSpy).toHaveBeenCalledWith(testSession); - expect(testSpy).toHaveBeenCalledWith(testCache, "lpar.service"); - expect(testCache.updateBaseProfileFileLogin).toHaveBeenCalledWith( + expect(testSpy).toHaveBeenCalledWith(blockMocks.testCache, "lpar.service"); + expect(updateBaseProfileFileLoginSpy).toHaveBeenCalledWith( { - name: tempBaseProfile.name, + ...tempBaseProfile, type: null, profile: { - ...serviceProfile.profile, + ...blockMocks.serviceProfile.profile, tokenType: tempBaseProfile.profile.tokenType, }, }, @@ -300,16 +368,20 @@ describe("ZoweVsCodeExtension", () => { quickPickMock.mockRestore(); }); it("should logout using the service profile given a simple profile name", async () => { - testCache.fetchBaseProfile.mockResolvedValue(baseProfile); + jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(blockMocks.baseProfile); + const updateBaseProfileFileLogoutSpy = jest.spyOn(blockMocks.testCache, "updateBaseProfileFileLogout"); const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "getServiceProfileForAuthPurposes"); - const newServiceProfile = { ...serviceProfile, profile: { ...testProfile, ...updProfile, host: "service" } }; + const newServiceProfile = { + ...blockMocks.serviceProfile, + profile: { ...blockMocks.testProfile, ...blockMocks.updProfile, host: "service" }, + }; testSpy.mockResolvedValue(newServiceProfile); const logoutSpy = jest.spyOn(Logout, "apimlLogout").mockImplementation(jest.fn()); const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); - await ZoweVsCodeExtension.logoutWithBaseProfile("service"); + await ZoweVsCodeExtension.ssoLogout({ serviceProfile: "service" }); - const testSession = new Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + const testSession = new imperative.Session(JSON.parse(JSON.stringify(blockMocks.expectedSession.ISession))); testSession.ISession.hostname = "service"; testSession.ISession.tokenValue = "tokenValue"; delete testSession.ISession.base64EncodedAuth; @@ -317,39 +389,51 @@ describe("ZoweVsCodeExtension", () => { delete testSession.ISession.password; expect(logoutSpy).toHaveBeenCalledWith(testSession); - expect(testSpy).toHaveBeenCalledWith(testCache, "service"); - expect(testCache.updateBaseProfileFileLogout).toHaveBeenCalledWith(newServiceProfile); + expect(testSpy).toHaveBeenCalledWith(blockMocks.testCache, "service"); + expect(updateBaseProfileFileLogoutSpy).toHaveBeenCalledWith(newServiceProfile); quickPickMock.mockRestore(); }); it("should login using the base profile when provided with a node, register, and cache instance", async () => { - testCache.fetchBaseProfile.mockResolvedValue(baseProfile); + jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(blockMocks.baseProfile); + const updateBaseProfileFileLoginSpy = jest.spyOn(blockMocks.testCache, "updateBaseProfileFileLogin"); const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "getServiceProfileForAuthPurposes"); 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(serviceProfile, "apimlAuthenticationToken", testNode, testRegister, testCache); - - const testSession = new Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + await ZoweVsCodeExtension.ssoLogin({ + serviceProfile: blockMocks.serviceProfile, + defaultTokenType: "apimlAuthenticationToken", + profileNode: blockMocks.testNode, + zeRegister: blockMocks.testRegister, + zeProfiles: blockMocks.testCache, + }); + + const testSession = new imperative.Session(JSON.parse(JSON.stringify(blockMocks.expectedSession.ISession))); testSession.ISession.base64EncodedAuth = "dXNlcjpwYXNz"; expect(loginSpy).not.toHaveBeenCalled(); expect(testSpy).not.toHaveBeenCalled(); - expect(testCache.updateBaseProfileFileLogin).toHaveBeenCalledWith(baseProfile, updProfile, false); - expect(testNode.setProfileToChoice).toHaveBeenCalled(); + expect(updateBaseProfileFileLoginSpy).toHaveBeenCalledWith(blockMocks.baseProfile, blockMocks.updProfile, false); + expect(blockMocks.testNode.setProfileToChoice).toHaveBeenCalled(); quickPickMock.mockRestore(); }); }); it("should logout using the base profile when provided with a node, register, and cache instance", async () => { - testCache.fetchBaseProfile.mockResolvedValue(baseProfile); + jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(blockMocks.baseProfile); + const updateBaseProfileFileLogoutSpy = jest.spyOn(blockMocks.testCache, "updateBaseProfileFileLogout"); const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "getServiceProfileForAuthPurposes"); const logoutSpy = jest.spyOn(Logout, "apimlLogout").mockImplementation(jest.fn()); - const newServiceProfile = { ...serviceProfile, profile: { ...testProfile, ...updProfile } }; + const newServiceProfile = { ...blockMocks.serviceProfile, profile: { ...blockMocks.testProfile, ...blockMocks.updProfile } }; - await ZoweVsCodeExtension.logoutWithBaseProfile(newServiceProfile, testRegister, testCache); + await ZoweVsCodeExtension.ssoLogout({ + serviceProfile: newServiceProfile, + zeRegister: blockMocks.testRegister, + zeProfiles: blockMocks.testCache, + }); - const testSession = new Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + const testSession = new imperative.Session(JSON.parse(JSON.stringify(blockMocks.expectedSession.ISession))); testSession.ISession.tokenValue = "tokenValue"; delete testSession.ISession.base64EncodedAuth; delete testSession.ISession.user; @@ -357,11 +441,12 @@ describe("ZoweVsCodeExtension", () => { expect(logoutSpy).not.toHaveBeenCalled(); expect(testSpy).not.toHaveBeenCalled(); - expect(testCache.updateBaseProfileFileLogout).toHaveBeenCalledWith(baseProfile); + expect(updateBaseProfileFileLogoutSpy).toHaveBeenCalledWith(blockMocks.baseProfile); }); it("calls promptCertificate if 'Certificate' was selected in quick pick", async () => { - testCache.fetchBaseProfile.mockResolvedValue(baseProfile); + jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(blockMocks.baseProfile); + const updateBaseProfileFileLoginSpy = jest.spyOn(blockMocks.testCache, "updateBaseProfileFileLogin"); const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "getServiceProfileForAuthPurposes"); jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass").mockResolvedValue(["user", "pass"]); let sessionCopy; @@ -373,25 +458,25 @@ describe("ZoweVsCodeExtension", () => { // case 1: User selects "user/password" for login quick pick const promptCertMock = jest.spyOn(ZoweVsCodeExtension as any, "promptCertificate").mockImplementation(); const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[1]); - await ZoweVsCodeExtension.loginWithBaseProfile("service"); + await ZoweVsCodeExtension.ssoLogin({ serviceProfile: "service" }); - const testSession = new Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + const testSession = new imperative.Session(JSON.parse(JSON.stringify(blockMocks.expectedSession.ISession))); delete testSession.ISession.user; delete testSession.ISession.password; delete testSession.ISession.base64EncodedAuth; testSession.ISession.storeCookie = false; expect(sessionCopy.ISession.type).toBe(imperative.SessConstants.AUTH_TYPE_CERT_PEM); - expect(testSpy).toHaveBeenCalledWith(testCache, "service"); + expect(testSpy).toHaveBeenCalledWith(blockMocks.testCache, "service"); expect(loginSpy).toHaveBeenCalledWith(sessionCopy); expect(promptCertMock).toHaveBeenCalled(); expect(quickPickMock).toHaveBeenCalled(); - expect(testCache.updateBaseProfileFileLogin).toHaveBeenCalledWith(baseProfile, updProfile, false); + expect(updateBaseProfileFileLoginSpy).toHaveBeenCalledWith(blockMocks.baseProfile, blockMocks.updProfile, false); promptCertMock.mockRestore(); }); it("returns false if there's an error from promptCertificate", async () => { - testCache.fetchBaseProfile.mockResolvedValue(baseProfile); + jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(blockMocks.baseProfile); jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass").mockResolvedValue(["user", "pass"]); // case 1: User selects "user/password" for login quick pick @@ -400,10 +485,190 @@ describe("ZoweVsCodeExtension", () => { const promptCertMock = jest .spyOn(ZoweVsCodeExtension as any, "promptCertificate") .mockRejectedValueOnce(new Error("invalid certificate")); - await expect(ZoweVsCodeExtension.loginWithBaseProfile("service")).resolves.toBe(false); + await expect(ZoweVsCodeExtension.ssoLogin({ serviceProfile: "service" })).resolves.toBe(false); expect(promptCertMock).toHaveBeenCalled(); expect(quickPickMock).toHaveBeenCalled(); }); + + it("should not login if the user cancels the operation when selecting the authentication method", async () => { + jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(blockMocks.baseProfile); + const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation(() => undefined as any); + const didLogin = await ZoweVsCodeExtension.ssoLogin({ + serviceProfile: blockMocks.serviceProfile, + defaultTokenType: "apimlAuthenticationToken", + profileNode: blockMocks.testNode, + zeRegister: blockMocks.testRegister, + zeProfiles: blockMocks.testCache, + }); + + expect(didLogin).toBeFalsy(); + quickPickMock.mockRestore(); + }); + + it("should prefer base profile token type even if the user does not provide credentials to login with", async () => { + const serviceProfileLoaded = { ...blockMocks.serviceProfile, profile: { ...blockMocks.serviceProfile.profile, tokenType: "SERVICE" } }; + const baseProfileLoaded = { ...blockMocks.baseProfile, profile: { ...blockMocks.baseProfile.profile, tokenType: "BASE" } }; + jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(baseProfileLoaded); + + const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass").mockResolvedValue(undefined); + const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); + const didLogin = await ZoweVsCodeExtension.ssoLogin({ + serviceProfile: serviceProfileLoaded, + defaultTokenType: "apimlAuthenticationToken", + profileNode: blockMocks.testNode, + zeRegister: blockMocks.testRegister, + zeProfiles: blockMocks.testCache, + preferBaseToken: true, // force to use base profile to login + }); + + const testSession = new imperative.Session(JSON.parse(JSON.stringify(blockMocks.expectedSession.ISession))); + testSession.ISession.base64EncodedAuth = "VXNlcm5hbWU6UGFzc3dvcmQ="; + delete testSession.ISession.user; + delete testSession.ISession.password; + expect(testSpy).toHaveBeenCalledWith({ + rePrompt: true, + session: { ...testSession.ISession, tokenType: "BASE" }, + }); + expect(didLogin).toBeFalsy(); + quickPickMock.mockRestore(); + }); + + it("should update the cache when autoStore is false after a successful login operation", async () => { + const serviceProfileLoaded = { ...blockMocks.serviceProfile, profile: { ...blockMocks.serviceProfile.profile, tokenType: "SERVICE" } }; + const baseProfileLoaded = { ...blockMocks.baseProfile, profile: { ...blockMocks.baseProfile.profile, tokenType: "BASE" } }; + jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(baseProfileLoaded); + blockMocks.configLayer.properties.autoStore = false; + + jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass").mockResolvedValue(["user", "pass"]); + const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); + await ZoweVsCodeExtension.profilesCache.refresh({ + registeredApiTypes: jest.fn().mockReturnValue(["service"]), + } as unknown as Types.IApiRegisterClient); + const didLogin = await ZoweVsCodeExtension.ssoLogin({ + serviceProfile: serviceProfileLoaded, + defaultTokenType: "apimlAuthenticationToken", + profileNode: blockMocks.testNode, + zeRegister: blockMocks.testRegister, + zeProfiles: blockMocks.testCache, + }); + + const expectedProfile = { + ...serviceProfileLoaded, + profile: { ...blockMocks.testProfile, tokenType: "SERVICE", tokenValue: "tokenValue" }, + }; + expect(didLogin).toBeTruthy(); + expect(blockMocks.testCache.allProfiles).toEqual([expectedProfile, blockMocks.baseProfile]); + quickPickMock.mockRestore(); + }); + + it("should prefer base profile when it exists and has tokenType defined", async () => { + const serviceProfileLoaded = { ...blockMocks.serviceProfile, profile: { ...blockMocks.serviceProfile.profile, tokenType: "SERVICE" } }; + const baseProfileLoaded = { ...blockMocks.baseProfile, profile: { ...blockMocks.baseProfile.profile, tokenType: "BASE" } }; + const fetchBaseProfileSpy = jest.spyOn(blockMocks.testCache, "fetchBaseProfile"); + blockMocks.configLayer.properties.profiles.service.properties.tokenType = "SERVICE"; + blockMocks.configLayer.properties.profiles.base.properties.tokenType = "BASE"; + blockMocks.configLayer.properties.defaults.base = "base"; + + const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass").mockResolvedValue(["abc", "def"]); + const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); + const didLogin = await ZoweVsCodeExtension.ssoLogin({ + serviceProfile: serviceProfileLoaded, + defaultTokenType: "apimlAuthenticationToken", + profileNode: blockMocks.testNode, + zeRegister: blockMocks.testRegister, + zeProfiles: blockMocks.testCache, + preferBaseToken: true, // force to use base profile to login + }); + + expect(testSpy).toHaveBeenCalled(); + expect(didLogin).toBe(true); + await expect(fetchBaseProfileSpy.mock.results[0].value).resolves.toEqual(baseProfileLoaded); + quickPickMock.mockRestore(); + }); + + it("should prefer base profile when it exists, does not have tokenType defined, and service profile is flat", async () => { + const serviceProfileLoaded = { ...blockMocks.serviceProfile, profile: { ...blockMocks.serviceProfile.profile, tokenType: "SERVICE" } }; + const baseProfileLoaded = { ...blockMocks.baseProfile, profile: { ...blockMocks.baseProfile.profile } }; + const fetchBaseProfileSpy = jest.spyOn(blockMocks.testCache, "fetchBaseProfile"); + blockMocks.configLayer.properties.profiles.service.properties.tokenType = "SERVICE"; + blockMocks.configLayer.properties.profiles.base.properties.tokenType = undefined; + blockMocks.configLayer.properties.defaults.base = "base"; + + const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass").mockResolvedValue(["abc", "def"]); + const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); + const didLogin = await ZoweVsCodeExtension.ssoLogin({ + serviceProfile: serviceProfileLoaded, + defaultTokenType: "apimlAuthenticationToken", + profileNode: blockMocks.testNode, + zeRegister: blockMocks.testRegister, + zeProfiles: blockMocks.testCache, + preferBaseToken: true, // force to use base profile to login + }); + + expect(testSpy).toHaveBeenCalled(); + expect(didLogin).toBe(true); + await expect(fetchBaseProfileSpy.mock.results[0].value).resolves.toEqual(baseProfileLoaded); + quickPickMock.mockRestore(); + }); + + it("should prefer parent profile when base profile does not exist and service profile is nested", async () => { + const serviceProfileLoaded = { + ...blockMocks.serviceProfile, + name: "lpar.service", + profile: { ...blockMocks.serviceProfile.profile, tokenType: "SERVICE" }, + }; + const baseProfileLoaded = { ...blockMocks.baseProfile, name: "lpar", profile: {} }; + const fetchBaseProfileSpy = jest.spyOn(blockMocks.testCache, "fetchBaseProfile"); + blockMocks.configLayer.properties.profiles = { + lpar: { + profiles: { + service: { + type: "service", + properties: [], + }, + }, + properties: [], + }, + }; + + const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass").mockResolvedValue(["abc", "def"]); + const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); + const didLogin = await ZoweVsCodeExtension.ssoLogin({ + serviceProfile: serviceProfileLoaded, + defaultTokenType: "apimlAuthenticationToken", + profileNode: blockMocks.testNode, + zeRegister: blockMocks.testRegister, + zeProfiles: blockMocks.testCache, + preferBaseToken: true, // force to use base profile to login + }); + + expect(testSpy).toHaveBeenCalled(); + expect(didLogin).toBe(true); + await expect(fetchBaseProfileSpy.mock.results[0].value).resolves.toEqual(baseProfileLoaded); + quickPickMock.mockRestore(); + }); + + it("should cancel the operation if the base profile does not exist and service profile is flat", async () => { + const serviceProfileLoaded = { ...blockMocks.serviceProfile, profile: { ...blockMocks.serviceProfile.profile, tokenType: "SERVICE" } }; + const fetchBaseProfileSpy = jest.spyOn(blockMocks.testCache, "fetchBaseProfile"); + delete blockMocks.configLayer.properties.profiles.base; + + const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass"); + const errorMessageSpy = jest.spyOn(Gui, "errorMessage"); + const didLogin = await ZoweVsCodeExtension.ssoLogin({ + serviceProfile: serviceProfileLoaded, + defaultTokenType: "apimlAuthenticationToken", + profileNode: blockMocks.testNode, + zeRegister: blockMocks.testRegister, + zeProfiles: blockMocks.testCache, + preferBaseToken: true, // force to use base profile to login + }); + + expect(testSpy).not.toHaveBeenCalled(); + expect(errorMessageSpy).toHaveBeenCalledWith(expect.stringContaining("No base profile found")); + expect(didLogin).toBe(false); + await expect(fetchBaseProfileSpy.mock.results[0].value).resolves.toBeUndefined(); + }); }); describe("updateCredentials", () => { const promptCredsOptions: PromptCredentialsOptions.ComplexOptions = { @@ -435,7 +700,7 @@ describe("ZoweVsCodeExtension", () => { expect(mockUpdateProperty).toHaveBeenCalledTimes(2); }); - it("should update user and password as secure fields with reprompt", async () => { + it("should update user and password as secure fields with rePrompt", async () => { const mockUpdateProperty = jest.fn(); jest.spyOn(ZoweVsCodeExtension as any, "profilesCache", "get").mockReturnValue({ getLoadedProfConfig: jest.fn().mockReturnValue({ diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts index ba8722c824..10d136a585 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts @@ -201,8 +201,8 @@ describe("Table.View", () => { globalMocks.updateWebviewMock.mockResolvedValueOnce(true); const cols = [ { field: "apple", valueFormatter: (data: { value: Table.ContentTypes }) => `${data.value.toString()} apples` }, - { field: "banana", comparator: (valueA, valueB, nodeA, nodeB, isDescending) => -1, colSpan: (params) => 2 }, - { field: "orange", rowSpan: (params) => 2 }, + { field: "banana", comparator: (_valueA, _valueB, _nodeA, _nodeB, _isDescending) => -1, colSpan: (_params) => 2 }, + { field: "orange", rowSpan: (_params) => 2 }, ]; await expect(view.setColumns(cols)).resolves.toBe(true); expect((view as any).data.columns).toStrictEqual( @@ -352,7 +352,7 @@ describe("Table.View", () => { command: "multi-action", callback: { typ: "multi-row", - fn: (_view: Table.View, row: Record) => { + fn: (_view: Table.View, _row: Record) => { multiCallbackMock(); }, }, @@ -364,7 +364,7 @@ describe("Table.View", () => { command: "zero-action", callback: { typ: "single-row", - fn: (_view: Table.View, row: Table.RowInfo) => { + fn: (_view: Table.View, _row: Table.RowInfo) => { zeroCallbackMock(); }, }, @@ -428,12 +428,12 @@ describe("Table.View", () => { it("sets the columns on the internal data structure and calls updateWebview", async () => { const globalMocks = createGlobalMocks(); const mockCols = [ - { field: "name", sort: "desc", colSpan: (params) => 2, rowSpan: (params) => 2 }, + { field: "name", sort: "desc", colSpan: (_params) => 2, rowSpan: (_params) => 2 }, { field: "address", sort: "asc", - comparator: (valueA, valueB, nodeA, nodeB, isDescending) => 1, - valueFormatter: (data: { value }) => `Located at ${data.value.toString()}`, + comparator: (_valueA, _valueB, _nodeA, _nodeB, _isDescending) => 1, + valueFormatter: (data: { value }) => `Located at ${(data.value as string).toString()}`, }, ] as Table.ColumnOpts[]; const data = { title: "Table w/ cols", columns: [], rows: [] }; diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts index 14d8768236..8639148f97 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts @@ -142,8 +142,8 @@ describe("TableBuilder", () => { const builder = new TableBuilder(globalMocks.context as any); const newCols: Table.ColumnOpts[] = [ { field: "cat", valueFormatter: (data: { value: Table.ContentTypes }) => `val: ${data.value.toString()}` }, - { field: "doge", filter: true, comparator: (valueA, valueB, nodeA, nodeB, isDescending) => -1, colSpan: (params) => 2 }, - { field: "parrot", sort: "asc", rowSpan: (params) => 2 }, + { field: "doge", filter: true, comparator: (_valueA, _valueB, _nodeA, _nodeB, _isDescending) => -1, colSpan: (_params) => 2 }, + { field: "parrot", sort: "asc", rowSpan: (_params) => 2 }, ]; expect((builder as any).convertColumnOpts(newCols)).toStrictEqual( newCols.map((col) => ({ diff --git a/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts b/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts index 830bb9bcda..6a4a9b9f0e 100644 --- a/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts +++ b/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts @@ -155,7 +155,7 @@ export class ProfilesCache { for (const type of allTypes) { const tmpAllProfiles: imperative.IProfileLoaded[] = []; // Step 1: Get all profiles for each registered type - const profilesForType = mProfileInfo.getAllProfiles(type).filter((temp) => temp.profLoc.osLoc.length !== 0); + const profilesForType = mProfileInfo.getAllProfiles(type).filter((temp) => temp.profLoc.osLoc?.length > 0); if (profilesForType && profilesForType.length > 0) { for (const prof of profilesForType) { // Step 2: Merge args for each profile @@ -322,14 +322,11 @@ export class ProfilesCache { const mProfileInfo = await this.getProfileInfo(); const baseProfileAttrs = mProfileInfo.getDefaultProfile("base"); const config = mProfileInfo.getTeamConfig(); - if ( - profileName?.includes(".") && - (baseProfileAttrs == null || !config.api.secure.securePropsForProfile(baseProfileAttrs.profName).includes("tokenValue")) - ) { + if (profileName?.includes(".") && (baseProfileAttrs == null || config.api.profiles.get(baseProfileAttrs.profName).tokenType == null)) { // Retrieve parent typeless profile as base profile if: // (1) The active profile name is nested (contains a period) AND // (2) No default base profile was found OR - // Default base profile does not have tokenValue in secure array + // Default base profile does not have tokenType defined const parentProfile = this.getParentProfileForToken(profileName, config); return this.getProfileLoaded(parentProfile, "base", config.api.profiles.get(parentProfile)); } else if (baseProfileAttrs == null) { diff --git a/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts b/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts index 25e889212a..4ea46ba295 100644 --- a/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts +++ b/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts @@ -17,6 +17,7 @@ import * as imperative from "@zowe/imperative"; import { Gui } from "../globals/Gui"; import { PromptCredentialsOptions } from "./doc/PromptCredentials"; import { Types } from "../Types"; +import { BaseProfileAuthOptions } from "./doc"; /** * Collection of utility functions for writing Zowe Explorer VS Code extensions. @@ -111,7 +112,8 @@ export class ZoweVsCodeExtension { * Trigger a login 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 stored in the service profile. * If there is no API registered for the profile type, this method defaults the login behavior to that of the APIML. - * @param serviceProfile Profile to be used for login pursposes (either the name of the IProfileLoaded instance) + * @deprecated Use `ZoweVsCodeExtension.ssoLogin` instead + * @param serviceProfile Profile to be used for login purposes (either the name of the IProfileLoaded instance) * @param loginTokenType The tokenType value for compatibility purposes * @param node The node for compatibility purposes * @param zeRegister The ZoweExplorerApiRegister instance for compatibility purposes @@ -124,17 +126,33 @@ export class ZoweVsCodeExtension { zeRegister?: Types.IApiRegisterClient, // ZoweExplorerApiRegister zeProfiles?: ProfilesCache // Profiles extends ProfilesCache ): Promise { - const cache: ProfilesCache = zeProfiles ?? ZoweVsCodeExtension.profilesCache; - if (typeof serviceProfile === "string") { - serviceProfile = await ZoweVsCodeExtension.getServiceProfileForAuthPurposes(cache, serviceProfile); - } + return this.ssoLogin({ serviceProfile, defaultTokenType: loginTokenType, profileNode: node, zeRegister, zeProfiles }); + } + + /** + * Trigger a login 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 stored in the service profile. + * If there is no API registered for the profile type, this method defaults the login behavior to that of the APIML. + * @param {BaseProfileAuthOptions} opts Object defining options for base profile authentication + */ + public static async ssoLogin(opts: BaseProfileAuthOptions): Promise { + const cache: ProfilesCache = opts.zeProfiles ?? ZoweVsCodeExtension.profilesCache; + const serviceProfile = + typeof opts.serviceProfile === "string" + ? await ZoweVsCodeExtension.getServiceProfileForAuthPurposes(cache, opts.serviceProfile) + : opts.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 primaryProfile = opts.preferBaseToken ? baseProfile : serviceProfile; + const secondaryProfile = opts.preferBaseToken ? serviceProfile : baseProfile; const tokenType = - serviceProfile.profile.tokenType ?? baseProfile.profile.tokenType ?? loginTokenType ?? imperative.SessConstants.TOKEN_TYPE_APIML; + primaryProfile.profile.tokenType ?? + secondaryProfile.profile.tokenType ?? + opts.defaultTokenType ?? + imperative.SessConstants.TOKEN_TYPE_APIML; const updSession = new imperative.Session({ hostname: serviceProfile.profile.host, port: serviceProfile.profile.port, @@ -173,7 +191,7 @@ export class ZoweVsCodeExtension { return false; } - const loginToken = await (zeRegister?.getCommonApi(serviceProfile).login ?? Login.apimlLogin)(updSession); + const loginToken = await (opts.zeRegister?.getCommonApi(serviceProfile).login ?? Login.apimlLogin)(updSession); const updBaseProfile: imperative.IProfile = { tokenType: updSession.ISession.tokenType ?? tokenType, tokenValue: loginToken, @@ -196,15 +214,8 @@ export class ZoweVsCodeExtension { } await cache.updateBaseProfileFileLogin(profileToUpdate, updBaseProfile, !connOk); - const baseIndex = cache.allProfiles.findIndex((profile) => profile.name === profileToUpdate.name); - cache.allProfiles[baseIndex] = { ...profileToUpdate, profile: { ...profileToUpdate.profile, ...updBaseProfile } }; - - if (node) { - node.setProfileToChoice({ - ...node.getProfile(), - profile: { ...node.getProfile().profile, ...updBaseProfile }, - }); - } + serviceProfile.profile = { ...serviceProfile.profile, ...updBaseProfile }; + await this.updateProfileInCache({ ...opts, serviceProfile }); return true; } @@ -212,7 +223,8 @@ export class ZoweVsCodeExtension { * 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. * If there is no API registered for the profile type, this method defaults the logout behavior to that of the APIML. - * @param serviceProfile Profile to be used for logout pursposes (either the name of the IProfileLoaded instance) + * @deprecated Use `ZoweVsCodeExtension.ssoLogout` instead + * @param serviceProfile Profile to be used for logout purposes (either the name of the IProfileLoaded instance) * @param zeRegister The ZoweExplorerApiRegister instance for compatibility purposes * @param zeProfiles The Zowe Explorer "Profiles.ts" instance for compatibility purposes */ @@ -221,38 +233,70 @@ export class ZoweVsCodeExtension { zeRegister?: Types.IApiRegisterClient, // ZoweExplorerApiRegister zeProfiles?: ProfilesCache // Profiles extends ProfilesCache ): Promise { - const cache: ProfilesCache = zeProfiles ?? ZoweVsCodeExtension.profilesCache; - if (typeof serviceProfile === "string") { - serviceProfile = await ZoweVsCodeExtension.getServiceProfileForAuthPurposes(cache, serviceProfile); - } - const baseProfile = await cache.fetchBaseProfile(serviceProfile.name); - if (baseProfile) { - const tokenType = - serviceProfile.profile.tokenType ?? - baseProfile.profile.tokenType ?? - zeRegister?.getCommonApi(serviceProfile).getTokenTypeName() ?? - imperative.SessConstants.TOKEN_TYPE_APIML; - const updSession = new imperative.Session({ - hostname: serviceProfile.profile.host, - port: serviceProfile.profile.port, - rejectUnauthorized: serviceProfile.profile.rejectUnauthorized, - tokenType: tokenType, - tokenValue: serviceProfile.profile.tokenValue ?? baseProfile.profile.tokenValue, - type: imperative.SessConstants.AUTH_TYPE_TOKEN, - }); - await (zeRegister?.getCommonApi(serviceProfile).logout ?? Logout.apimlLogout)(updSession); + return this.ssoLogout({ serviceProfile, zeRegister, zeProfiles }); + } - // If active profile is nested (e.g. lpar.zosmf), then update service profile since base profile may be typeless - 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 { + /** + * 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. + * If there is no API registered for the profile type, this method defaults the logout behavior to that of the APIML. + * @param {BaseProfileAuthOptions} opts Object defining options for base profile authentication + */ + public static async ssoLogout(opts: BaseProfileAuthOptions): Promise { + const cache: ProfilesCache = opts.zeProfiles ?? ZoweVsCodeExtension.profilesCache; + const serviceProfile = + typeof opts.serviceProfile === "string" + ? await ZoweVsCodeExtension.getServiceProfileForAuthPurposes(cache, opts.serviceProfile) + : opts.serviceProfile; + + 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}"`); return false; } + const primaryProfile = opts.preferBaseToken ? baseProfile : serviceProfile; + const secondaryProfile = opts.preferBaseToken ? serviceProfile : baseProfile; + const tokenType = + primaryProfile.profile.tokenType ?? + secondaryProfile.profile.tokenType ?? + opts.defaultTokenType ?? + imperative.SessConstants.TOKEN_TYPE_APIML; + const updSession = new imperative.Session({ + hostname: serviceProfile.profile.host, + port: serviceProfile.profile.port, + rejectUnauthorized: serviceProfile.profile.rejectUnauthorized, + tokenType: tokenType, + tokenValue: primaryProfile.profile.tokenValue ?? secondaryProfile.profile.tokenValue, + type: imperative.SessConstants.AUTH_TYPE_TOKEN, + }); + await (opts.zeRegister?.getCommonApi(serviceProfile).logout ?? Logout.apimlLogout)(updSession); + + // If active profile is nested (e.g. lpar.zosmf), then update service profile since base profile may be typeless + 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); + serviceProfile.profile = { ...serviceProfile.profile, tokenType: undefined, tokenValue: undefined }; + await this.updateProfileInCache({ ...opts, serviceProfile }); + return true; + } + + private static async updateProfileInCache(opts: BaseProfileAuthOptions & { serviceProfile: imperative.IProfileLoaded }): Promise { + const cache: ProfilesCache = opts.zeProfiles ?? ZoweVsCodeExtension.profilesCache; + if ((await cache.getProfileInfo()).getTeamConfig().properties.autoStore !== false) { + await cache.refresh(); + } else { + // Note: It should be expected that nested profiles within this service profile will have their credentials updated. + const profIndex = cache.allProfiles.findIndex((profile) => profile.name === opts.serviceProfile.name); + cache.allProfiles[profIndex] = { ...cache.allProfiles[profIndex], profile: opts.serviceProfile.profile }; + } + if (opts.profileNode) { + opts.profileNode.setProfileToChoice({ + ...opts.profileNode.getProfile(), + profile: { ...opts.profileNode.getProfile().profile, ...opts.serviceProfile.profile }, + }); + } } /** diff --git a/packages/zowe-explorer-api/src/vscode/doc/BaseProfileAuth.ts b/packages/zowe-explorer-api/src/vscode/doc/BaseProfileAuth.ts new file mode 100644 index 0000000000..72a520b2a7 --- /dev/null +++ b/packages/zowe-explorer-api/src/vscode/doc/BaseProfileAuth.ts @@ -0,0 +1,54 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import * as imperative from "@zowe/imperative"; +import { Types } from "../../Types"; +import { ProfilesCache } from "../../profiles"; + +/** + * Interface containing the properties used for centralized auth operations (e.g. login/logout) + */ +export interface BaseProfileAuthOptions { + /** + * The service profile used to perform the auth operation + * If the specified value is a string, we use it as the profile name. Note: Nested profiles are allowed here, e.g. lpar.service. + * If the specified value is a loaded profile, it should contain all properties already merged by the ProfileInfo APIs + */ + serviceProfile: string | imperative.IProfileLoaded; + + /** + * The type of token to use as a fallback whenever a token type cannot be identified from the service, parent, or base profiles + * For example: "apimlAuthenticationToken" + */ + defaultTokenType?: string; + + /** + * Indicates whether the auth operation should give precedence to the base profile in cases where there are conflicts. + * You may use this only if you need to override the default behavior. + * For details on the default behavior, see the flowchart in: https://github.com/zowe/zowe-explorer-vscode/issues/2264#issuecomment-2236914511 + */ + preferBaseToken?: boolean; + + /** + * The node used for this auth operation which will need to be explicitly updated/refresh after a successful auth operation. + */ + profileNode?: Types.IZoweNodeType; + + /** + * The instance of the ProfilesCache used to perform a cache refresh after a successful auth operation. + */ + zeProfiles?: ProfilesCache; + + /** + * The instance of the API Register from which we will get the auth method provided by extenders (if available). + */ + zeRegister?: Types.IApiRegisterClient; +} diff --git a/packages/zowe-explorer-api/src/vscode/doc/index.ts b/packages/zowe-explorer-api/src/vscode/doc/index.ts index ad1c1f0890..be56790557 100644 --- a/packages/zowe-explorer-api/src/vscode/doc/index.ts +++ b/packages/zowe-explorer-api/src/vscode/doc/index.ts @@ -9,4 +9,5 @@ * */ +export * from "./BaseProfileAuth"; export * from "./PromptCredentials"; diff --git a/packages/zowe-explorer/CHANGELOG.md b/packages/zowe-explorer/CHANGELOG.md index 1af7b7f42e..c1c40f0fff 100644 --- a/packages/zowe-explorer/CHANGELOG.md +++ b/packages/zowe-explorer/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen ### Bug fixes - The "Zowe Resources" panel is now hidden by default until Zowe Explorer reveals it to display a table or other data. [#3113](https://github.com/zowe/zowe-explorer-vscode/issues/3113) +- Fixed behavior of logout action when token is defined in both base profile and parent profile. [#3076](https://github.com/zowe/zowe-explorer-vscode/issues/3076) ## `3.0.0-next.202409132122` diff --git a/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts index b9cd7af9d8..7ab2b03e48 100644 --- a/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts @@ -1210,7 +1210,7 @@ describe("Profiles Unit Tests - function ssoLogin", () => { getTokenTypeName: () => imperative.SessConstants.TOKEN_TYPE_APIML, login: jest.fn(), } as never); - const loginBaseProfMock = jest.spyOn(ZoweVsCodeExtension, "loginWithBaseProfile").mockRejectedValueOnce(new Error("test error.")); + 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(); @@ -1309,7 +1309,7 @@ describe("Profiles Unit Tests - function handleSwitchAuthentication", () => { getTokenTypeName: () => "apimlAuthenticationToken", } as never); - jest.spyOn(ZoweVsCodeExtension, "loginWithBaseProfile").mockResolvedValue(true); + jest.spyOn(ZoweVsCodeExtension, "ssoLogin").mockResolvedValue(true); await Profiles.getInstance().handleSwitchAuthentication(testNode); expect(Gui.showMessage).toHaveBeenCalled(); expect(testNode.profile.profile.tokenType).toBe(modifiedTestNode.profile.profile.tokenType); @@ -1368,7 +1368,7 @@ describe("Profiles Unit Tests - function handleSwitchAuthentication", () => { getTokenTypeName: () => "apimlAuthenticationToken", } as never); - jest.spyOn(ZoweVsCodeExtension, "loginWithBaseProfile").mockResolvedValue(false); + jest.spyOn(ZoweVsCodeExtension, "ssoLogin").mockResolvedValue(false); await Profiles.getInstance().handleSwitchAuthentication(testNode); expect(Gui.errorMessage).toHaveBeenCalled(); expect(testNode.profile.profile.tokenType).toBe(modifiedTestNode.profile.profile.tokenType); diff --git a/packages/zowe-explorer/__tests__/__unit__/zosconsole/ZosConsolePanel.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/zosconsole/ZosConsolePanel.unit.test.ts index a7bba2ca5f..7260408033 100644 --- a/packages/zowe-explorer/__tests__/__unit__/zosconsole/ZosConsolePanel.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/zosconsole/ZosConsolePanel.unit.test.ts @@ -14,6 +14,11 @@ import { ZosConsoleViewProvider } from "../../../src/zosconsole/ZosConsolePanel" import { Profiles } from "../../../src/configuration/Profiles"; import * as vscode from "vscode"; +jest.mock("@zowe/zowe-explorer-api", () => ({ + ...jest.requireActual("@zowe/zowe-explorer-api"), + HTMLTemplate: jest.requireActual("../../../../zowe-explorer-api/src/vscode/ui/utils/HTMLTemplate"), +})); + describe("ZosConsoleViewProvider", () => { function createGlobalMocks(): any { const newMocks = { @@ -42,7 +47,7 @@ describe("ZosConsoleViewProvider", () => { const globalMocks = createGlobalMocks(); const myconsole = new ZosConsoleViewProvider({} as any); myconsole.resolveWebviewView(globalMocks.testWebView, {} as any, { isCancellationRequested: false } as any); - expect(globalMocks.testWebView.webview.onDidReceiveMessage).toBeCalled(); + expect(globalMocks.testWebView.webview.onDidReceiveMessage).toHaveBeenCalled(); }); }); }); diff --git a/packages/zowe-explorer/src/configuration/Profiles.ts b/packages/zowe-explorer/src/configuration/Profiles.ts index 9c9081ae68..d8f3cea1db 100644 --- a/packages/zowe-explorer/src/configuration/Profiles.ts +++ b/packages/zowe-explorer/src/configuration/Profiles.ts @@ -787,7 +787,14 @@ export class Profiles extends ProfilesCache { if (loginTokenType && !loginTokenType.startsWith(imperative.SessConstants.TOKEN_TYPE_APIML)) { loginOk = await this.loginWithRegularProfile(serviceProfile, node); } else { - loginOk = await ZoweVsCodeExtension.loginWithBaseProfile(serviceProfile, loginTokenType, node, zeInstance, this); + loginOk = await ZoweVsCodeExtension.ssoLogin({ + serviceProfile, + defaultTokenType: loginTokenType, + profileNode: node, + zeRegister: zeInstance, + zeProfiles: this, + preferBaseToken: true, + }); } if (loginOk) { Gui.showMessage( @@ -885,7 +892,14 @@ export class Profiles extends ProfilesCache { case AuthUtils.isProfileUsingBasicAuth(serviceProfile): { let loginOk = false; if (loginTokenType && loginTokenType.startsWith("apimlAuthenticationToken")) { - loginOk = await ZoweVsCodeExtension.loginWithBaseProfile(serviceProfile, loginTokenType, node, zeInstance, this); + loginOk = await ZoweVsCodeExtension.ssoLogin({ + serviceProfile, + defaultTokenType: loginTokenType, + profileNode: node, + zeRegister: zeInstance, + zeProfiles: this, + preferBaseToken: true, + }); } else { loginOk = await this.loginWithRegularProfile(serviceProfile, node); } @@ -1016,7 +1030,15 @@ export class Profiles extends ProfilesCache { ) { await ZoweExplorerApiRegister.getInstance().getCommonApi(serviceProfile).logout(node.getSession()); } else { - logoutOk = await ZoweVsCodeExtension.logoutWithBaseProfile(serviceProfile, ZoweExplorerApiRegister.getInstance(), this); + const zeRegister = ZoweExplorerApiRegister.getInstance(); + logoutOk = await ZoweVsCodeExtension.ssoLogout({ + serviceProfile, + defaultTokenType: zeRegister?.getCommonApi(serviceProfile).getTokenTypeName(), + profileNode: node, + zeRegister, + zeProfiles: this, + preferBaseToken: true, + }); } if (logoutOk) { Gui.showMessage(