diff --git a/packages/zowe-explorer/CHANGELOG.md b/packages/zowe-explorer/CHANGELOG.md index 8b192bfde8..ed99da5385 100644 --- a/packages/zowe-explorer/CHANGELOG.md +++ b/packages/zowe-explorer/CHANGELOG.md @@ -171,6 +171,12 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen - Added `madge` script in `package.json` to track circular dependencies. [#2148](https://github.com/zowe/vscode-extension-for-zowe/issues/2148) - Migrated to new package manager PNPM from Yarn. +## `2.17.0` + +### New features and enhancements + +- To add the ability to switch between basic authentication and token-based authentication. [#3062](https://github.com/zowe/zowe-explorer-vscode/pull/3062) + ## `2.16.2` ### Bug fixes @@ -179,7 +185,7 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen ## `2.16.0` -## New features and enhancements +### New features and enhancements - Added Status bar to indicate that data is being pulled from mainframe. [#2484](https://github.com/zowe/zowe-explorer-vscode/issues/2484) - Updated MVS view progress indicator for entering a filter search. [#2181](https://github.com/zowe/zowe-explorer-vscode/issues/2181) 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 a59d8008cc..2e50ad00fe 100644 --- a/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts @@ -1169,6 +1169,577 @@ describe("Profiles Unit Tests - function ssoLogin", () => { }); }); +describe("Profiles Unit Tests - function handleSwitchAuthentication", () => { + let testNode; + let globalMocks; + let modifiedTestNode; + + afterEach(() => { + jest.resetAllMocks(); + }); + + beforeEach(async () => { + globalMocks = await createGlobalMocks(); + testNode = new (ZoweTreeNode as any)( + "test", + vscode.TreeItemCollapsibleState.None, + undefined, + globalMocks.testSession, + globalMocks.testProfile + ); + + modifiedTestNode = new (ZoweTreeNode as any)( + "test", + vscode.TreeItemCollapsibleState.None, + undefined, + globalMocks.testSession, + globalMocks.testProfile + ); + }); + + it("To switch from Basic to Token-based authentication using Base Profile", async () => { + jest.spyOn(AuthUtils, "isProfileUsingBasicAuth").mockReturnValueOnce(true); + jest.spyOn(Profiles.getInstance(), "getProfileInfo").mockResolvedValue({ + getTeamConfig: () => ({ + properties: jest.fn(), + set: jest.fn(), + delete: jest.fn(), + save: jest.fn(), + }), + getAllProfiles: () => [ + { + profName: "sestest", + profLoc: { + osLoc: ["test"], + }, + }, + ], + mergeArgsForProfile: jest.fn().mockReturnValue({ + knownArgs: [ + { + argName: "user", + dataType: "string", + argValue: "fake", + argLoc: { jsonLoc: "jsonLoc" }, + }, + { + argName: "password", + dataType: "string", + argValue: "fake", + argLoc: { jsonLoc: "jsonLoc" }, + }, + ], + }), + } as any); + testNode.profile.profile = { + type: "zosmf", + host: "test", + port: 1443, + name: "base", + rejectUnauthorized: false, + user: "testUser", + password: "***", + tokenType: undefined, + tokenValue: undefined, + secure: ["user", "password"], + }; + modifiedTestNode.profile.profile = { + type: "zosmf", + host: "test", + port: 1443, + name: "base", + rejectUnauthorized: false, + user: undefined, + password: undefined, + tokenType: "testToken", + tokenValue: "12345", + secure: ["tokenType"], + }; + jest.spyOn(Gui, "resolveQuickPick").mockResolvedValue({ label: "Yes" } as vscode.QuickPickItem); + jest.spyOn(ZoweExplorerApiRegister.getInstance(), "getCommonApi").mockReturnValue({ + getTokenTypeName: () => "apimlAuthenticationToken", + } as never); + + jest.spyOn(ZoweVsCodeExtension, "loginWithBaseProfile").mockResolvedValue(true); + await Profiles.getInstance().handleSwitchAuthentication(testNode); + expect(Gui.showMessage).toHaveBeenCalled(); + expect(testNode.profile.profile.tokenType).toBe(modifiedTestNode.profile.profile.tokenType); + expect(testNode.profile.profile.tokenValue).toBe(modifiedTestNode.profile.profile.tokenValue); + expect(testNode.profile.profile.secure.length).toBe(modifiedTestNode.profile.profile.secure.length); + expect(testNode.profile.profile.secure).toEqual(modifiedTestNode.profile.profile.secure); + expect(testNode.profile.profile.user).toBeUndefined(); + expect(testNode.profile.profile.password).toBeUndefined(); + }); + + it("To check login fail's when trying to switch from Basic to Token-based authentication using Base Profile", async () => { + jest.spyOn(AuthUtils, "isProfileUsingBasicAuth").mockReturnValueOnce(true); + jest.spyOn(Profiles.getInstance(), "getProfileInfo").mockResolvedValue({ + getTeamConfig: () => ({ + properties: jest.fn(), + set: jest.fn(), + delete: jest.fn(), + save: jest.fn(), + }), + getAllProfiles: () => [ + { + profName: "sestest", + profLoc: { + osLoc: ["test"], + }, + }, + ], + } as any); + testNode.profile.profile = { + type: "zosmf", + host: "test", + port: 1443, + name: "base", + rejectUnauthorized: false, + user: "testUser", + password: "***", + tokenType: undefined, + tokenValue: undefined, + secure: ["user", "password"], + }; + modifiedTestNode.profile.profile = { + type: "zosmf", + host: "test", + port: 1443, + name: "base", + rejectUnauthorized: false, + user: "testUser", + password: "***", + tokenType: undefined, + tokenValue: undefined, + secure: ["user", "password"], + }; + jest.spyOn(Gui, "resolveQuickPick").mockResolvedValue({ label: "Yes" } as vscode.QuickPickItem); + jest.spyOn(Gui, "errorMessage").mockImplementation(); + jest.spyOn(ZoweExplorerApiRegister.getInstance(), "getCommonApi").mockReturnValue({ + getTokenTypeName: () => "apimlAuthenticationToken", + } as never); + + jest.spyOn(ZoweVsCodeExtension, "loginWithBaseProfile").mockResolvedValue(false); + await Profiles.getInstance().handleSwitchAuthentication(testNode); + expect(Gui.errorMessage).toHaveBeenCalled(); + expect(testNode.profile.profile.tokenType).toBe(modifiedTestNode.profile.profile.tokenType); + expect(testNode.profile.profile.tokenValue).toBe(modifiedTestNode.profile.profile.tokenValue); + expect(testNode.profile.profile.secure.length).toBe(modifiedTestNode.profile.profile.secure.length); + expect(testNode.profile.profile.secure).toEqual(modifiedTestNode.profile.profile.secure); + expect(testNode.profile.profile.tokenType).toBeUndefined(); + expect(testNode.profile.profile.tokenValue).toBeUndefined(); + }); + + it("To switch from Basic to Token-based authentication using Regular Profile", async () => { + jest.spyOn(AuthUtils, "isProfileUsingBasicAuth").mockReturnValueOnce(true); + jest.spyOn(Profiles.getInstance(), "getProfileInfo").mockResolvedValue({ + getTeamConfig: () => ({ + properties: jest.fn(), + set: jest.fn(), + delete: jest.fn(), + save: jest.fn(), + }), + getAllProfiles: () => [ + { + profName: "sestest", + profLoc: { + osLoc: ["test"], + }, + }, + ], + mergeArgsForProfile: jest.fn().mockReturnValue({ + knownArgs: [ + { + argName: "user", + dataType: "string", + argValue: "fake", + argLoc: { jsonLoc: "jsonLoc" }, + }, + { + argName: "password", + dataType: "string", + argValue: "fake", + argLoc: { jsonLoc: "jsonLoc" }, + }, + ], + }), + } as any); + testNode.profile.profile = { + type: "zosmf", + host: "test", + port: 1443, + name: "base", + rejectUnauthorized: false, + user: "testUser", + password: "***", + tokenType: undefined, + tokenValue: undefined, + secure: ["user", "password"], + }; + modifiedTestNode.profile.profile = { + type: "zosmf", + host: "test", + port: 1443, + name: "base", + rejectUnauthorized: false, + user: undefined, + password: undefined, + tokenType: "testToken", + tokenValue: "12345", + secure: ["tokenType"], + }; + jest.spyOn(Gui, "resolveQuickPick").mockResolvedValue({ label: "Yes" } as vscode.QuickPickItem); + jest.spyOn(ZoweExplorerApiRegister.getInstance(), "getCommonApi").mockReturnValue({ + getTokenTypeName: () => "jwtToken", + } as never); + jest.spyOn(Profiles.getInstance() as any, "loginWithRegularProfile").mockResolvedValue(true); + await Profiles.getInstance().handleSwitchAuthentication(testNode); + expect(Gui.showMessage).toHaveBeenCalled(); + expect(testNode.profile.profile.tokenType).toBe(modifiedTestNode.profile.profile.tokenType); + expect(testNode.profile.profile.tokenValue).toBe(modifiedTestNode.profile.profile.tokenValue); + expect(testNode.profile.profile.secure.length).toBe(modifiedTestNode.profile.profile.secure.length); + expect(testNode.profile.profile.secure).toEqual(modifiedTestNode.profile.profile.secure); + expect(testNode.profile.profile.user).toBeUndefined(); + expect(testNode.profile.profile.password).toBeUndefined(); + }); + + it("To check login fail's when trying to switch from Basic to Token-based authentication using Regular Profile", async () => { + jest.spyOn(AuthUtils, "isProfileUsingBasicAuth").mockReturnValueOnce(true); + jest.spyOn(Profiles.getInstance(), "getProfileInfo").mockResolvedValue({ + getTeamConfig: () => ({ + properties: jest.fn(), + set: jest.fn(), + delete: jest.fn(), + save: jest.fn(), + }), + getAllProfiles: () => [ + { + profName: "sestest", + profLoc: { + osLoc: ["test"], + }, + }, + ], + } as any); + testNode.profile.profile = { + type: "zosmf", + host: "test", + port: 1443, + name: "base", + rejectUnauthorized: false, + user: "testUser", + password: "***", + tokenType: undefined, + tokenValue: undefined, + secure: ["user", "password"], + }; + modifiedTestNode.profile.profile = { + type: "zosmf", + host: "test", + port: 1443, + name: "base", + rejectUnauthorized: false, + user: "testUser", + password: "***", + tokenType: undefined, + tokenValue: undefined, + secure: ["user", "password"], + }; + jest.spyOn(Gui, "resolveQuickPick").mockResolvedValue({ label: "Yes" } as vscode.QuickPickItem); + jest.spyOn(Gui, "errorMessage").mockImplementation(); + 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); + expect(testNode.profile.profile.tokenValue).toBe(modifiedTestNode.profile.profile.tokenValue); + expect(testNode.profile.profile.secure.length).toBe(modifiedTestNode.profile.profile.secure.length); + expect(testNode.profile.profile.secure).toEqual(modifiedTestNode.profile.profile.secure); + expect(testNode.profile.profile.tokenType).toBeUndefined(); + expect(testNode.profile.profile.tokenValue).toBeUndefined(); + }); + + it("To switch from Token-based to Basic authentication when cred values are passed involving base profile", async () => { + jest.spyOn(AuthUtils, "isUsingTokenAuth").mockResolvedValueOnce(true); + jest.spyOn(Profiles.getInstance(), "getProfileInfo").mockResolvedValue({ + getTeamConfig: () => ({ + properties: jest.fn(), + set: jest.fn(), + delete: jest.fn(), + save: jest.fn(), + }), + getAllProfiles: () => [ + { + profName: "sestest", + profLoc: { + osLoc: ["test"], + jsonLoc: "jsonLoc", + }, + }, + { + profName: "base", + profLoc: { locType: 0, osLoc: ["location"], jsonLoc: "jsonLoc" }, + }, + ], + mergeArgsForProfile: jest.fn().mockReturnValue({ + knownArgs: [ + { + argName: "user", + dataType: "string", + argValue: "fake", + argLoc: { jsonLoc: "jsonLoc" }, + }, + { + argName: "password", + dataType: "string", + argValue: "fake", + argLoc: { jsonLoc: "jsonLoc" }, + }, + ], + }), + } as any); + testNode.profile.profile = { + type: "zosmf", + host: "test", + port: 1443, + name: "base", + rejectUnauthorized: false, + user: undefined, + password: undefined, + tokenType: "testTokenType", + tokenValue: "12345", + secure: ["tokenType"], + }; + modifiedTestNode.profile.profile = { + type: "zosmf", + host: "test", + port: 1443, + name: "base", + rejectUnauthorized: false, + user: "testUser", + password: "6789", + tokenType: undefined, + tokenValue: undefined, + secure: ["user", "password"], + }; + jest.spyOn(Gui, "resolveQuickPick").mockResolvedValue({ label: "Yes" } as vscode.QuickPickItem); + jest.spyOn(ZoweExplorerApiRegister.getInstance(), "getCommonApi").mockReturnValue({ + getTokenTypeName: () => "apimlAuthenticationToken", + } as never); + jest.spyOn(Profiles.getInstance(), "promptCredentials").mockResolvedValue(["testUser", "6789"]); + await Profiles.getInstance().handleSwitchAuthentication(testNode); + expect(Gui.showMessage).toHaveBeenCalled(); + expect(Gui.showMessage).toHaveBeenCalled(); + expect(testNode.profile.profile.tokenType).toBe(modifiedTestNode.profile.profile.tokenType); + expect(testNode.profile.profile.tokenValue).toBe(modifiedTestNode.profile.profile.tokenValue); + expect(testNode.profile.profile.secure.length).toBe(modifiedTestNode.profile.profile.secure.length); + expect(testNode.profile.profile.secure).toEqual(modifiedTestNode.profile.profile.secure); + expect(testNode.profile.profile.tokenType).toBeUndefined(); + expect(testNode.profile.profile.tokenValue).toBeUndefined(); + }); + + it("To switch from Token-based to Basic authentication when cred values are passed involving regular profile", async () => { + jest.spyOn(AuthUtils, "isUsingTokenAuth").mockResolvedValueOnce(true); + jest.spyOn(Profiles.getInstance(), "getProfileInfo").mockResolvedValue({ + getTeamConfig: () => ({ + properties: jest.fn(), + set: jest.fn(), + delete: jest.fn(), + save: jest.fn(), + }), + getAllProfiles: () => [ + { + profName: "sestest", + profLoc: { + osLoc: ["test"], + }, + }, + ], + mergeArgsForProfile: jest.fn().mockReturnValue({ + knownArgs: [ + { + argName: "user", + dataType: "string", + argValue: "fake", + argLoc: { jsonLoc: "jsonLoc" }, + }, + { + argName: "password", + dataType: "string", + argValue: "fake", + argLoc: { jsonLoc: "jsonLoc" }, + }, + ], + }), + } as any); + testNode.profile.profile = { + type: "zosmf", + host: "test", + port: 1443, + name: "base", + rejectUnauthorized: false, + user: undefined, + password: undefined, + tokenType: "testTokenType", + tokenValue: "12345", + secure: ["tokenType"], + }; + modifiedTestNode.profile.profile = { + type: "zosmf", + host: "test", + port: 1443, + name: "base", + rejectUnauthorized: false, + user: "testUser", + password: "6789", + tokenType: undefined, + tokenValue: undefined, + secure: ["user", "password"], + }; + jest.spyOn(Gui, "resolveQuickPick").mockResolvedValue({ label: "Yes" } as vscode.QuickPickItem); + jest.spyOn(ZoweExplorerApiRegister.getInstance(), "getCommonApi").mockReturnValue({ + getTokenTypeName: () => "jwtToken", + } as never); + jest.spyOn(Profiles.getInstance(), "promptCredentials").mockResolvedValue(["testUser", "6789"]); + await Profiles.getInstance().handleSwitchAuthentication(testNode); + expect(Gui.showMessage).toHaveBeenCalled(); + expect(Gui.showMessage).toHaveBeenCalled(); + expect(testNode.profile.profile.tokenType).toBe(modifiedTestNode.profile.profile.tokenType); + expect(testNode.profile.profile.tokenValue).toBe(modifiedTestNode.profile.profile.tokenValue); + expect(testNode.profile.profile.secure.length).toBe(modifiedTestNode.profile.profile.secure.length); + expect(testNode.profile.profile.secure).toEqual(modifiedTestNode.profile.profile.secure); + expect(testNode.profile.profile.tokenType).toBeUndefined(); + expect(testNode.profile.profile.tokenValue).toBeUndefined(); + }); + + it("To not switch from Token-based to Basic authentication when cred values are not passed", async () => { + jest.spyOn(AuthUtils, "isUsingTokenAuth").mockResolvedValueOnce(true); + jest.spyOn(Profiles.getInstance(), "getProfileInfo").mockResolvedValue({ + getTeamConfig: () => ({ + properties: jest.fn(), + set: jest.fn(), + delete: jest.fn(), + save: jest.fn(), + }), + getAllProfiles: () => [ + { + profName: "sestest", + profLoc: { + osLoc: ["test"], + }, + }, + ], + } as any); + testNode.profile.profile = { + type: "zosmf", + host: "test", + port: 1443, + name: "base", + rejectUnauthorized: false, + user: undefined, + password: undefined, + tokenType: "testTokenType", + tokenValue: "12345", + secure: ["tokenType"], + }; + modifiedTestNode.profile.profile = { + type: "zosmf", + host: "test", + port: 1443, + name: "base", + rejectUnauthorized: false, + user: undefined, + password: undefined, + tokenType: "testTokenType", + tokenValue: "12345", + secure: ["tokenType"], + }; + jest.spyOn(Gui, "resolveQuickPick").mockResolvedValue({ label: "Yes" } as vscode.QuickPickItem); + jest.spyOn(ZoweExplorerApiRegister.getInstance(), "getCommonApi").mockReturnValue({ + getTokenTypeName: () => "apimlAuthenticationToken", + } as never); + jest.spyOn(Gui, "errorMessage").mockImplementation(); + jest.spyOn(Profiles.getInstance(), "promptCredentials").mockResolvedValue(undefined as any); + await Profiles.getInstance().handleSwitchAuthentication(testNode); + expect(Gui.errorMessage).toHaveBeenCalled(); + expect(testNode.profile.profile.tokenType).toBe(modifiedTestNode.profile.profile.tokenType); + expect(testNode.profile.profile.tokenValue).toBe(modifiedTestNode.profile.profile.tokenValue); + expect(testNode.profile.profile.secure.length).toBe(modifiedTestNode.profile.profile.secure.length); + expect(testNode.profile.profile.secure).toEqual(modifiedTestNode.profile.profile.secure); + expect(testNode.profile.profile.user).toBeUndefined(); + expect(testNode.profile.profile.password).toBeUndefined(); + }); + + it("To not perform switching the authentication for a profile which does not support token-based authentication", async () => { + jest.spyOn(AuthUtils, "isProfileUsingBasicAuth").mockReturnValueOnce(true); + jest.spyOn(Gui, "errorMessage").mockImplementation(); + jest.spyOn(Gui, "resolveQuickPick").mockResolvedValue({ label: "Yes" } as vscode.QuickPickItem); + jest.spyOn(ZoweExplorerApiRegister.getInstance(), "getCommonApi").mockReturnValue({ + getTokenTypeName: () => { + throw new Error("test error."); + }, + } as never); + await Profiles.getInstance().handleSwitchAuthentication(testNode); + expect(Gui.errorMessage).toHaveBeenCalled(); + }); + + it("To not perform switching the authentication when authentication method is unknown", async () => { + jest.spyOn(Gui, "resolveQuickPick").mockResolvedValue({ label: "Yes" } as vscode.QuickPickItem); + jest.spyOn(Gui, "errorMessage").mockImplementation(); + jest.spyOn(AuthUtils, "isProfileUsingBasicAuth").mockReturnValueOnce(false); + jest.spyOn(AuthUtils, "isUsingTokenAuth").mockResolvedValueOnce(false); + jest.spyOn(ZoweExplorerApiRegister.getInstance(), "getCommonApi").mockReturnValue({ + getTokenTypeName: () => "apimlAuthenticationToken", + } as never); + await Profiles.getInstance().handleSwitchAuthentication(testNode); + expect(Gui.errorMessage).toHaveBeenCalled(); + }); + + it("To not perform switching the authentication when user wants to cancel the authentication switch", async () => { + testNode.profile.profile = { + type: "zosmf", + host: "test", + port: 1443, + name: "base", + rejectUnauthorized: false, + user: undefined, + password: undefined, + tokenType: "testTokenType", + tokenValue: "12345", + secure: ["tokenType"], + }; + modifiedTestNode.profile.profile = { + type: "zosmf", + host: "test", + port: 1443, + name: "base", + rejectUnauthorized: false, + user: undefined, + password: undefined, + tokenType: "testTokenType", + tokenValue: "12345", + secure: ["tokenType"], + }; + jest.spyOn(Gui, "resolveQuickPick").mockResolvedValue({ label: "No" } as vscode.QuickPickItem); + await Profiles.getInstance().handleSwitchAuthentication(testNode); + expect(testNode.profile.profile.tokenType).toBe(modifiedTestNode.profile.profile.tokenType); + expect(testNode.profile.profile.tokenValue).toBe(modifiedTestNode.profile.profile.tokenValue); + expect(testNode.profile.profile.secure.length).toBe(modifiedTestNode.profile.profile.secure.length); + expect(testNode.profile.profile.secure).toEqual(modifiedTestNode.profile.profile.secure); + expect(testNode.profile.profile.user).toBeUndefined(); + expect(testNode.profile.profile.password).toBeUndefined(); + }); + + it("To not perform switching the authentication when user wants escapes the quick pick of authentication switch", async () => { + jest.spyOn(Gui, "resolveQuickPick").mockResolvedValue(undefined); + jest.spyOn(Gui, "infoMessage").mockImplementation(); + await Profiles.getInstance().handleSwitchAuthentication(testNode); + expect(Gui.infoMessage).toHaveBeenCalled(); + }); +}); + describe("Profiles Unit Tests - function ssoLogout", () => { let testNode; let globalMocks; diff --git a/packages/zowe-explorer/__tests__/__unit__/management/ProfileManagement.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/management/ProfileManagement.unit.test.ts index 51a2770591..203cc8296a 100644 --- a/packages/zowe-explorer/__tests__/__unit__/management/ProfileManagement.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/management/ProfileManagement.unit.test.ts @@ -51,6 +51,7 @@ describe("ProfileManagement unit tests", () => { mockUpdateChosen: ProfileManagement.basicAuthUpdateQpItems[ProfileManagement.AuthQpLabels.update], mockAddBasicChosen: ProfileManagement.basicAuthAddQpItems[ProfileManagement.AuthQpLabels.add], mockLoginChosen: ProfileManagement.tokenAuthLoginQpItem[ProfileManagement.AuthQpLabels.login], + mockSwitchChosen: ProfileManagement.switchAuthenticationQpItems[ProfileManagement.AuthQpLabels.switch], mockLogoutChosen: ProfileManagement.tokenAuthLogoutQpItem[ProfileManagement.AuthQpLabels.logout], mockEditProfChosen: ProfileManagement.editProfileQpItems[ProfileManagement.AuthQpLabels.edit], mockDeleteProfChosen: ProfileManagement.deleteProfileQpItem[ProfileManagement.AuthQpLabels.delete], @@ -64,6 +65,7 @@ describe("ProfileManagement unit tests", () => { promptSpy: null as any, editSpy: null as any, loginSpy: null as any, + handleSwitchAuthenticationSpy: null as any, logoutSpy: null as any, logMsg: null as any, commandSpy: null as any, @@ -105,6 +107,8 @@ describe("ProfileManagement unit tests", () => { newMocks.editSpy = jest.spyOn(newMocks.mockProfileInstance, "editSession"); Object.defineProperty(newMocks.mockProfileInstance, "ssoLogin", { value: jest.fn(), configurable: true }); newMocks.loginSpy = jest.spyOn(newMocks.mockProfileInstance, "ssoLogin"); + Object.defineProperty(newMocks.mockProfileInstance, "handleSwitchAuthentication", { value: jest.fn(), configurable: true }); + newMocks.handleSwitchAuthenticationSpy = jest.spyOn(newMocks.mockProfileInstance, "handleSwitchAuthentication"); Object.defineProperty(newMocks.mockProfileInstance, "ssoLogout", { value: jest.fn(), configurable: true }); newMocks.logoutSpy = jest.spyOn(newMocks.mockProfileInstance, "ssoLogout"); Object.defineProperty(vscode.commands, "executeCommand", { value: jest.fn(), configurable: true }); @@ -115,6 +119,14 @@ describe("ProfileManagement unit tests", () => { } describe("unit tests around basic auth selections", () => { + beforeEach(() => { + jest.resetModules(); + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + function createBlockMocks(globalMocks): any { globalMocks.logMsg = `Profile ${globalMocks.mockBasicAuthProfile.name as string} is using basic authentication.`; globalMocks.mockDsSessionNode.getProfile = jest.fn().mockReturnValue(globalMocks.mockBasicAuthProfile); @@ -134,6 +146,12 @@ describe("ProfileManagement unit tests", () => { expect(mocks.debugLogSpy).toHaveBeenCalledWith(mocks.logMsg); expect(mocks.promptSpy).toHaveBeenCalled(); }); + it("profile using basic authentication should see handleSwitchAuthentication called when Change the Authentication method chosen", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockSwitchAuthChosen); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toHaveBeenCalledWith(mocks.logMsg); + }); it("profile using basic authentication should see editSession called when Edit Profile chosen", async () => { const mocks = createBlockMocks(createGlobalMocks()); mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockEditProfChosen); @@ -166,6 +184,13 @@ describe("ProfileManagement unit tests", () => { }); }); describe("unit tests around token auth selections", () => { + beforeEach(() => { + jest.resetModules(); + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); function createBlockMocks(globalMocks): any { globalMocks.logMsg = `Profile ${globalMocks.mockTokenAuthProfile.name as string} is using token authentication.`; globalMocks.mockUnixSessionNode = unixMock.createUSSSessionNode(globalMocks.mockSession, globalMocks.mockBasicAuthProfile) as any; @@ -219,6 +244,13 @@ describe("ProfileManagement unit tests", () => { expect(mocks.debugLogSpy).toHaveBeenCalledWith(mocks.logMsg); expect(mocks.commandSpy).toHaveBeenLastCalledWith("zowe.disableValidation", mocks.mockUnixSessionNode); }); + it("profile using token authentication should see handleSwitchAuthentication called when Change the Authentication method chosen", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + jest.spyOn(AuthUtils, "isUsingTokenAuth").mockResolvedValue(true); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockSwitchAuthChosen); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toHaveBeenCalledWith(mocks.logMsg); + }); }); describe("unit tests around no auth declared selections", () => { function createBlockMocks(globalMocks): any { diff --git a/packages/zowe-explorer/l10n/bundle.l10n.json b/packages/zowe-explorer/l10n/bundle.l10n.json index 99d4a8b1b8..d2f4241f5f 100644 --- a/packages/zowe-explorer/l10n/bundle.l10n.json +++ b/packages/zowe-explorer/l10n/bundle.l10n.json @@ -172,6 +172,24 @@ "Required API functions for pasting (fileList and copy/uploadFromBuffer) were not found.": "Required API functions for pasting (fileList and copy/uploadFromBuffer) were not found.", "Uploading USS files...": "Uploading USS files...", "Error uploading files": "Error uploading files", + "The 'move' function is not implemented for this USS API.": "The 'move' function is not implemented for this USS API.", + "Could not list USS files: Empty path provided in URI": "Could not list USS files: Empty path provided in URI", + "Profile does not exist for this file.": "Profile does not exist for this file.", + "$(sync~spin) Saving USS file...": "$(sync~spin) Saving USS file...", + "Renaming {0} failed due to API error: {1}/File pathError message": { + "message": "Renaming {0} failed due to API error: {1}", + "comment": [ + "File path", + "Error message" + ] + }, + "Deleting {0} failed due to API error: {1}/File nameError message": { + "message": "Deleting {0} failed due to API error: {1}", + "comment": [ + "File name", + "Error message" + ] + }, "Downloaded: {0}/Download time": { "message": "Downloaded: {0}", "comment": [ @@ -242,24 +260,6 @@ "initializeUSSFavorites.error.buttonRemove": "initializeUSSFavorites.error.buttonRemove", "File does not exist. It may have been deleted.": "File does not exist. It may have been deleted.", "$(sync~spin) Pulling from Mainframe...": "$(sync~spin) Pulling from Mainframe...", - "The 'move' function is not implemented for this USS API.": "The 'move' function is not implemented for this USS API.", - "Could not list USS files: Empty path provided in URI": "Could not list USS files: Empty path provided in URI", - "Profile does not exist for this file.": "Profile does not exist for this file.", - "$(sync~spin) Saving USS file...": "$(sync~spin) Saving USS file...", - "Renaming {0} failed due to API error: {1}/File pathError message": { - "message": "Renaming {0} failed due to API error: {1}", - "comment": [ - "File path", - "Error message" - ] - }, - "Deleting {0} failed due to API error: {1}/File nameError message": { - "message": "Deleting {0} failed due to API error: {1}", - "comment": [ - "File name", - "Error message" - ] - }, "{0} location/Node type": { "message": "{0} location", "comment": [ @@ -847,6 +847,8 @@ "Update profile connection information": "Update profile connection information", "$(eye-closed) Hide Profile": "$(eye-closed) Hide Profile", "Hide profile name from tree view": "Hide profile name from tree view", + "$(key) Change the Authentication Method": "$(key) Change the Authentication Method", + "Change the authentication method": "Change the authentication method", "$(arrow-right) Log in to authentication service": "$(arrow-right) Log in to authentication service", "Log in to obtain a new token value": "Log in to obtain a new token value", "$(arrow-left) Log out of authentication service": "$(arrow-left) Log out of authentication service", @@ -969,6 +971,15 @@ "Error message" ] }, + "To change the authentication": "To change the authentication", + "To continue in current authentication": "To continue in current authentication", + "Do you wish to change the Authentication": "Do you wish to change the Authentication", + "Cannot switch to Token-based Authentication for profile {0}.": "Cannot switch to Token-based Authentication for profile {0}.", + "Login using token-based authentication service was successful for profile {0}.": "Login using token-based authentication service was successful for profile {0}.", + "Unable to switch to Token-based authentication for profile {0}.": "Unable to switch to Token-based authentication for profile {0}.", + "Login using basic authentication was successful for profile {0}.": "Login using basic authentication was successful for profile {0}.", + "Unable to switch to Basic authentication for profile {0}.": "Unable to switch to Basic authentication for profile {0}.", + "Unable to Switch Authentication for profile {0}.": "Unable to Switch Authentication for profile {0}.", "Logout from authentication service was successful for {0}./Service profile name": { "message": "Logout from authentication service was successful for {0}.", "comment": [ diff --git a/packages/zowe-explorer/l10n/poeditor.json b/packages/zowe-explorer/l10n/poeditor.json index af04fc4edb..04a8a5b31b 100644 --- a/packages/zowe-explorer/l10n/poeditor.json +++ b/packages/zowe-explorer/l10n/poeditor.json @@ -489,6 +489,12 @@ "Required API functions for pasting (fileList and copy/uploadFromBuffer) were not found.": "", "Uploading USS files...": "", "Error uploading files": "", + "The 'move' function is not implemented for this USS API.": "", + "Could not list USS files: Empty path provided in URI": "", + "Profile does not exist for this file.": "", + "$(sync~spin) Saving USS file...": "", + "Renaming {0} failed due to API error: {1}": "", + "Deleting {0} failed due to API error: {1}": "", "Downloaded: {0}": "", "Encoding: {0}": "", "Binary": "", @@ -517,12 +523,6 @@ "initializeUSSFavorites.error.buttonRemove": "", "File does not exist. It may have been deleted.": "", "$(sync~spin) Pulling from Mainframe...": "", - "The 'move' function is not implemented for this USS API.": "", - "Could not list USS files: Empty path provided in URI": "", - "Profile does not exist for this file.": "", - "$(sync~spin) Saving USS file...": "", - "Renaming {0} failed due to API error: {1}": "", - "Deleting {0} failed due to API error: {1}": "", "{0} location": "", "Choose a location to create the {0}": "", "Name of file or directory": "", @@ -763,6 +763,8 @@ "Update profile connection information": "", "$(eye-closed) Hide Profile": "", "Hide profile name from tree view": "", + "$(key) Change the Authentication Method": "", + "Change the authentication method": "", "$(arrow-right) Log in to authentication service": "", "Log in to obtain a new token value": "", "$(arrow-left) Log out of authentication service": "", @@ -803,6 +805,15 @@ "Error getting supported tokenType value for profile {0}": "", "Login to authentication service was successful for {0}.": "", "Unable to log in with {0}. {1}": "", + "To change the authentication": "", + "To continue in current authentication": "", + "Do you wish to change the Authentication": "", + "Cannot switch to Token-based Authentication for profile {0}.": "", + "Login using token-based authentication service was successful for profile {0}.": "", + "Unable to switch to Token-based authentication for profile {0}.": "", + "Login using basic authentication was successful for profile {0}.": "", + "Unable to switch to Basic authentication for profile {0}.": "", + "Unable to Switch Authentication for profile {0}.": "", "Logout from authentication service was successful for {0}.": "", "Unable to log out with {0}. {1}": "", "Select the location where the config file will be initialized": "", diff --git a/packages/zowe-explorer/src/configuration/Profiles.ts b/packages/zowe-explorer/src/configuration/Profiles.ts index 2e00e5ebec..32047cba24 100644 --- a/packages/zowe-explorer/src/configuration/Profiles.ts +++ b/packages/zowe-explorer/src/configuration/Profiles.ts @@ -763,6 +763,127 @@ export class Profiles extends ProfilesCache { } } + public async basicAuthClearSecureArray(profileName?: string, loginTokenType?: string): Promise { + const profInfo = await this.getProfileInfo(); + const configApi = profInfo.getTeamConfig(); + const profAttrs = await this.getProfileFromConfig(profileName); + if (loginTokenType && loginTokenType.startsWith("apimlAuthenticationToken")) { + configApi.set(`${profAttrs.profLoc.jsonLoc}.secure`, []); + } else { + configApi.set(`${profAttrs.profLoc.jsonLoc}.secure`, ["tokenValue"]); + } + configApi.delete(profInfo.mergeArgsForProfile(profAttrs).knownArgs.find((arg) => arg.argName === "user")?.argLoc.jsonLoc); + configApi.delete(profInfo.mergeArgsForProfile(profAttrs).knownArgs.find((arg) => arg.argName === "password")?.argLoc.jsonLoc); + await configApi.save(); + } + + public async tokenAuthClearSecureArray(profileName?: string, loginTokenType?: string): Promise { + const profInfo = await this.getProfileInfo(); + const configApi = profInfo.getTeamConfig(); + if (loginTokenType && loginTokenType.startsWith("apimlAuthenticationToken")) { + const profAttrs = await this.getProfileFromConfig("base"); + configApi.set(`${profAttrs.profLoc.jsonLoc}.secure`, []); + configApi.delete(profInfo.mergeArgsForProfile(profAttrs).knownArgs.find((arg) => arg.argName === "tokenType")?.argLoc.jsonLoc); + configApi.delete(profInfo.mergeArgsForProfile(profAttrs).knownArgs.find((arg) => arg.argName === "tokenValue")?.argLoc.jsonLoc); + configApi.delete(profInfo.mergeArgsForProfile(profAttrs).knownArgs.find((arg) => arg.argName === "tokenExpiration")?.argLoc.jsonLoc); + } else { + const profAttrs = await this.getProfileFromConfig(profileName); + configApi.set(`${profAttrs.profLoc.jsonLoc}.secure`, ["user", "password"]); + configApi.delete(profInfo.mergeArgsForProfile(profAttrs).knownArgs.find((arg) => arg.argName === "tokenType")?.argLoc.jsonLoc); + configApi.delete(profInfo.mergeArgsForProfile(profAttrs).knownArgs.find((arg) => arg.argName === "tokenValue")?.argLoc.jsonLoc); + configApi.delete(profInfo.mergeArgsForProfile(profAttrs).knownArgs.find((arg) => arg.argName === "tokenExpiration")?.argLoc.jsonLoc); + } + await configApi.save(); + } + + public async handleSwitchAuthentication(node: Types.IZoweNodeType): Promise { + const qp = Gui.createQuickPick(); + const qpItemYes: vscode.QuickPickItem = { + label: vscode.l10n.t("Yes"), + description: vscode.l10n.t("To change the authentication"), + }; + const qpItemNo: vscode.QuickPickItem = { + label: vscode.l10n.t("No"), + description: vscode.l10n.t("To continue in current authentication"), + }; + qp.items = [qpItemYes, qpItemNo]; + qp.placeholder = vscode.l10n.t("Do you wish to change the Authentication"); + qp.activeItems = [qpItemYes]; + qp.show(); + const qpSelection = await Gui.resolveQuickPick(qp); + qp.hide(); + + if (qpSelection === undefined) { + Gui.infoMessage(vscode.l10n.t("Operation Cancelled")); + return; + } + if (qpSelection.label === vscode.l10n.t("No")) { + return; + } + + let loginTokenType: string; + const serviceProfile = node.getProfile() ?? this.loadNamedProfile(node.label.toString().trim()); + const zeInstance = ZoweExplorerApiRegister.getInstance(); + try { + loginTokenType = await zeInstance.getCommonApi(serviceProfile).getTokenTypeName(); + } catch (error) { + ZoweLogger.warn(error); + Gui.errorMessage(vscode.l10n.t("Cannot switch to Token-based Authentication for profile {0}.", serviceProfile.name)); + return; + } + switch (true) { + case AuthUtils.isProfileUsingBasicAuth(serviceProfile): { + let loginOk = false; + if (loginTokenType && loginTokenType.startsWith("apimlAuthenticationToken")) { + loginOk = await ZoweVsCodeExtension.loginWithBaseProfile(serviceProfile, loginTokenType, node, zeInstance, this); + } else { + loginOk = await this.loginWithRegularProfile(serviceProfile, node); + } + + if (loginOk) { + Gui.showMessage( + vscode.l10n.t("Login using token-based authentication service was successful for profile {0}.", serviceProfile.name) + ); + await this.basicAuthClearSecureArray(serviceProfile.name, loginTokenType); + const updBaseProfile: imperative.IProfile = { + user: undefined, + password: undefined, + }; + node.setProfileToChoice({ + ...node.getProfile(), + profile: { ...node.getProfile().profile, ...updBaseProfile }, + }); + } else { + Gui.errorMessage(vscode.l10n.t("Unable to switch to Token-based authentication for profile {0}.", serviceProfile.name)); + return; + } + break; + } + case await AuthUtils.isUsingTokenAuth(serviceProfile.name): { + const profile: string | imperative.IProfileLoaded = node.getProfile(); + const creds = await Profiles.getInstance().promptCredentials(profile, true); + + if (creds !== undefined) { + const successMsg = vscode.l10n.t( + "Login using basic authentication was successful for profile {0}.", + typeof profile === "string" ? profile : profile.name + ); + ZoweLogger.info(successMsg); + Gui.showMessage(successMsg); + await this.tokenAuthClearSecureArray(serviceProfile.name, loginTokenType); + ZoweExplorerApiRegister.getInstance().onProfilesUpdateEmitter.fire(Validation.EventType.UPDATE); + } else { + Gui.errorMessage(vscode.l10n.t("Unable to switch to Basic authentication for profile {0}.", serviceProfile.name)); + return; + } + break; + } + default: { + Gui.errorMessage(vscode.l10n.t("Unable to Switch Authentication for profile {0}.", serviceProfile.name)); + } + } + } + public clearDSFilterFromTree(node: Types.IZoweNodeType): void { if (!SharedTreeProviders.ds?.mSessionNodes || !SharedTreeProviders.ds?.mSessionNodes.length) { return; diff --git a/packages/zowe-explorer/src/management/ProfileManagement.ts b/packages/zowe-explorer/src/management/ProfileManagement.ts index e2ebea1e35..26df74a749 100644 --- a/packages/zowe-explorer/src/management/ProfileManagement.ts +++ b/packages/zowe-explorer/src/management/ProfileManagement.ts @@ -68,6 +68,7 @@ export class ProfileManagement { edit: "edit-profile", enable: "enable-validation", hide: "hide-profile", + switch: "switch-auth", login: "obtain-token", logout: "invalidate-token", update: "update-credentials", @@ -113,6 +114,12 @@ export class ProfileManagement { description: vscode.l10n.t("Hide profile name from tree view"), }, }; + public static switchAuthenticationQpItems: Record = { + [ProfileManagement.AuthQpLabels.switch]: { + label: vscode.l10n.t("$(key) Change the Authentication Method"), + description: vscode.l10n.t("Change the authentication method"), + }, + }; public static tokenAuthLoginQpItem: Record = { [ProfileManagement.AuthQpLabels.login]: { label: vscode.l10n.t("$(arrow-right) Log in to authentication service"), @@ -181,6 +188,10 @@ export class ProfileManagement { await this.handleHideProfiles(node); break; } + case this.switchAuthenticationQpItems[this.AuthQpLabels.switch]: { + await Profiles.getInstance().handleSwitchAuthentication(node); + break; + } case this.deleteProfileQpItem[this.AuthQpLabels.delete]: { await this.handleDeleteProfiles(node); break; @@ -222,6 +233,7 @@ export class ProfileManagement { private static basicAuthQp(node: IZoweTreeNode): vscode.QuickPickItem[] { const quickPickOptions: vscode.QuickPickItem[] = Object.values(this.basicAuthUpdateQpItems); + quickPickOptions.push(this.switchAuthenticationQpItems[this.AuthQpLabels.switch]); return this.addFinalQpOptions(node, quickPickOptions); } private static tokenAuthQp(node: IZoweTreeNode): vscode.QuickPickItem[] { @@ -230,6 +242,7 @@ export class ProfileManagement { if (profile.profile.tokenValue) { quickPickOptions.push(this.tokenAuthLogoutQpItem[this.AuthQpLabels.logout]); } + quickPickOptions.push(this.switchAuthenticationQpItems[this.AuthQpLabels.switch]); return this.addFinalQpOptions(node, quickPickOptions); } private static chooseAuthQp(node: IZoweTreeNode): vscode.QuickPickItem[] {