diff --git a/packages/zowe-explorer/CHANGELOG.md b/packages/zowe-explorer/CHANGELOG.md index c96a16391a..b4815bcd01 100644 --- a/packages/zowe-explorer/CHANGELOG.md +++ b/packages/zowe-explorer/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen ### New features and enhancements - Added "Sort Jobs" feature for job nodes in Jobs tree view. [#2257](https://github.com/zowe/vscode-extension-for-zowe/issues/2251) +- Introduce a new user interface for managing profiles via right-click action "Manage Profile". - Added new edit feature on `Edit Attributes` view for changing file tags on USS [#2113](https://github.com/zowe/vscode-extension-for-zowe/issues/2113) - Added new API {ZE Extender MetaData} to allow extenders to have the metadata of registered extenders to aid in team configuration file creation from a view that isn't Zowe Explorer's. [#2394](https://github.com/zowe/vscode-extension-for-zowe/issues/2394) diff --git a/packages/zowe-explorer/__mocks__/mockCreators/shared.ts b/packages/zowe-explorer/__mocks__/mockCreators/shared.ts index fb08566cf2..07d17faaf5 100644 --- a/packages/zowe-explorer/__mocks__/mockCreators/shared.ts +++ b/packages/zowe-explorer/__mocks__/mockCreators/shared.ts @@ -218,6 +218,40 @@ export function createValidIProfile(): imperative.IProfileLoaded { }; } +export function createTokenAuthIProfile(): imperative.IProfileLoaded { + return { + name: "sestest", + profile: { + type: "zosmf", + host: "test", + port: 1443, + rejectUnauthorized: false, + tokenType: "apimlAuthenticationToken", + tokenValue: "stringofletters", + name: "testName", + }, + type: "zosmf", + message: "", + failNotFound: false, + }; +} + +export function createNoAuthIProfile(): imperative.IProfileLoaded { + return { + name: "sestest", + profile: { + type: "zosmf", + host: null, + port: 1443, + rejectUnauthorized: false, + name: "testName", + }, + type: "zosmf", + message: "", + failNotFound: false, + }; +} + export function createAltTypeIProfile(): imperative.IProfileLoaded { return { name: "altTypeProfile", diff --git a/packages/zowe-explorer/__tests__/__theia__/theia/Locators.ts b/packages/zowe-explorer/__tests__/__theia__/theia/Locators.ts index 089c6386bc..5e3eb996bc 100644 --- a/packages/zowe-explorer/__tests__/__theia__/theia/Locators.ts +++ b/packages/zowe-explorer/__tests__/__theia__/theia/Locators.ts @@ -28,7 +28,7 @@ export const DatasetsLocators = { favoriteProfileInDatasetXpath: "(//div[contains(@id,'Favorites') and contains(@id,'TestSeleniumProfile')])", addToFavoriteOptionXpath: "//li[@data-command='zowe.ds.saveSearch']", removeFavoriteProfileFromDatasetsOptionXpath: "//li[@data-command='zowe.ds.removeFavProfile']", - deleteProfileFromDatasetsXpath: "(//li[@data-command='zowe.ds.deleteProfile'])", + manageProfileFromDatasetsXpath: "(//li[@data-command='zowe.profileManagement'])", }; export const UssLocators = { @@ -44,6 +44,7 @@ export const UssLocators = { addToFavoriteOptionXpath: "//li[@data-command='zowe.uss.addFavorite']", removeFavoriteProfileFromUssOptionXpath: "//li[@data-command='zowe.uss.removeFavProfile']", hideProfileFromUssOptionXpath: "//li[@data-command='zowe.uss.removeSession']", + manageProfileFromUnixXpath: "(//li[@data-command='zowe.profileManagement'])", }; export const JobsLocators = { @@ -60,6 +61,7 @@ export const JobsLocators = { removeFavoriteProfileFromJobsOptionXpath: "//li[@data-command='zowe.jobs.removeFavProfile']", hideProfileFromJobsOptionXpath: "//li[@data-command='zowe.jobs.removeJobsSession']", secondJobsProfileBeforeHidingXpath: "(//div[contains(@id,'TestSeleniumProfile')])[2]", + manageProfileFromJobsXpath: "(//li[@data-command='zowe.profileManagement'])", }; export const TheiaNotificationMessages = { diff --git a/packages/zowe-explorer/__tests__/__theia__/theia/extension.theiaChrome.ts b/packages/zowe-explorer/__tests__/__theia__/theia/extension.theiaChrome.ts index d9bd97df36..239aadb46a 100644 --- a/packages/zowe-explorer/__tests__/__theia__/theia/extension.theiaChrome.ts +++ b/packages/zowe-explorer/__tests__/__theia__/theia/extension.theiaChrome.ts @@ -138,13 +138,21 @@ export async function addProfileToFavoritesInJobs() { export async function hideProfileInUss() { const hideProfileFromUss = await driverChrome.wait(until.elementLocated(By.xpath(UssLocators.secondUssProfileXpath)), WAITTIME); await driverChrome.actions().click(hideProfileFromUss, Button.RIGHT).perform(); - await driverChrome.wait(until.elementLocated(By.xpath(UssLocators.hideProfileFromUssOptionXpath)), WAITTIME).click(); + driverChrome.wait(until.elementLocated(By.xpath(UssLocators.manageProfileFromUnixXpath)), WAITTIME).click(); + await driverChrome.sleep(SHORTSLEEPTIME); + const manageProfile = driverChrome.wait(until.elementLocated(By.xpath(UssLocators.emptyInputBoxXpath)), WAITTIME); + manageProfile.sendKeys("Hide Profile"); + manageProfile.sendKeys(Key.ENTER); } export async function hideProfileInJobs() { const hideProfileFromJobs = await driverChrome.wait(until.elementLocated(By.xpath(JobsLocators.secondJobsProfileBeforeHidingXpath)), WAITTIME); await driverChrome.actions().click(hideProfileFromJobs, Button.RIGHT).perform(); - await driverChrome.wait(until.elementLocated(By.xpath(JobsLocators.hideProfileFromJobsOptionXpath)), WAITTIME).click(); + driverChrome.wait(until.elementLocated(By.xpath(JobsLocators.manageProfileFromJobsXpath)), WAITTIME).click(); + await driverChrome.sleep(SHORTSLEEPTIME); + const manageProfile = driverChrome.wait(until.elementLocated(By.xpath(JobsLocators.emptyInputBoxXpath)), WAITTIME); + manageProfile.sendKeys("Hide Profile"); + manageProfile.sendKeys(Key.ENTER); } export async function verifyProfileIsHideInUss() { @@ -170,23 +178,29 @@ export async function verifyProfileIsHideInJobs() { export async function deleteDefaultProfileInDatasets() { const profileName = await driverChrome.wait(until.elementLocated(By.xpath(DatasetsLocators.defaultDatasetsProfileXpath)), WAITTIME); await driverChrome.actions().click(profileName, Button.RIGHT).perform(); - await driverChrome.wait(until.elementLocated(By.xpath(DatasetsLocators.deleteProfileFromDatasetsXpath)), WAITTIME).click(); + await driverChrome.wait(until.elementLocated(By.xpath(DatasetsLocators.manageProfileFromDatasetsXpath)), WAITTIME).click(); + await driverChrome.sleep(SHORTSLEEPTIME); + const manageProfile = driverChrome.wait(until.elementLocated(By.xpath(DatasetsLocators.emptyInputBoxXpath)), WAITTIME); + manageProfile.sendKeys("Delete Profile"); + manageProfile.sendKeys(Key.ENTER); await driverChrome.sleep(SHORTSLEEPTIME); const deleteProfile = driverChrome.wait(until.elementLocated(By.xpath(DatasetsLocators.emptyInputBoxXpath)), WAITTIME); deleteProfile.sendKeys("Delete"); deleteProfile.sendKeys(Key.ENTER); - return; } export async function deleteProfileInDatasets() { const favprofile = await driverChrome.wait(until.elementLocated(By.xpath(DatasetsLocators.secondDatasetProfileXpath)), WAITTIME); await driverChrome.actions().click(favprofile, Button.RIGHT).perform(); - await driverChrome.wait(until.elementLocated(By.xpath(DatasetsLocators.deleteProfileFromDatasetsXpath)), WAITTIME).click(); + await driverChrome.wait(until.elementLocated(By.xpath(DatasetsLocators.manageProfileFromDatasetsXpath)), WAITTIME).click(); + await driverChrome.sleep(SHORTSLEEPTIME); + const manageProfile = driverChrome.wait(until.elementLocated(By.xpath(DatasetsLocators.emptyInputBoxXpath)), WAITTIME); + manageProfile.sendKeys("Delete Profile"); + manageProfile.sendKeys(Key.ENTER); await driverChrome.sleep(SHORTSLEEPTIME); const deleteProfile = driverChrome.wait(until.elementLocated(By.xpath(DatasetsLocators.emptyInputBoxXpath)), WAITTIME); deleteProfile.sendKeys("Delete"); deleteProfile.sendKeys(Key.ENTER); - return; } export async function verifyRemovedFavoriteProfileInDatasets() { diff --git a/packages/zowe-explorer/__tests__/__unit__/Profiles.extended.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/Profiles.extended.unit.test.ts index 2876032d7b..6d41665a89 100644 --- a/packages/zowe-explorer/__tests__/__unit__/Profiles.extended.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/Profiles.extended.unit.test.ts @@ -1376,7 +1376,7 @@ describe("Profiles Unit Tests - function checkCurrentProfile", () => { it("should throw an error if using token auth and is logged out or has expired token", async () => { const globalMocks = await createGlobalMocks(); jest.spyOn(utils, "errorHandling").mockImplementation(); - jest.spyOn(utils, "isUsingTokenAuth").mockResolvedValue(true); + jest.spyOn(utils.ProfilesUtils, "isUsingTokenAuth").mockResolvedValue(true); setupProfilesCheck(globalMocks); await expect(Profiles.getInstance().checkCurrentProfile(globalMocks.testProfile)).resolves.toEqual({ name: "sestest", status: "unverified" }); }); @@ -1569,6 +1569,7 @@ describe("Profiles Unit Tests - function ssoLogin", () => { ], configurable: true, }); + Object.defineProperty(utils.ProfilesUtils, "isProfileUsingBasicAuth", { value: jest.fn(), configurable: true }); jest.spyOn(Gui, "showMessage").mockImplementation(); }); it("should perform an SSOLogin successfully while fetching the base profile", async () => { diff --git a/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts index 0f637993a6..5f111c1d64 100644 --- a/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts @@ -244,6 +244,7 @@ async function createGlobalMocks() { "zowe.manualPoll", "zowe.updateSecureCredentials", "zowe.promptCredentials", + "zowe.profileManagement", "zowe.openRecentMember", "zowe.searchInAllLoadedItems", "zowe.ds.deleteProfile", diff --git a/packages/zowe-explorer/__tests__/__unit__/utils/ProfileManagement.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/utils/ProfileManagement.unit.test.ts new file mode 100644 index 0000000000..2cd395f690 --- /dev/null +++ b/packages/zowe-explorer/__tests__/__unit__/utils/ProfileManagement.unit.test.ts @@ -0,0 +1,259 @@ +/** + * 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 { ZoweDatasetNode } from "../../../src/dataset/ZoweDatasetNode"; +import * as sharedMock from "../../../__mocks__/mockCreators/shared"; +import * as dsMock from "../../../__mocks__/mockCreators/datasets"; +import * as unixMock from "../../../__mocks__/mockCreators/uss"; +import * as profUtils from "../../../src/utils/ProfilesUtils"; +import { ProfileManagement } from "../../../src/utils/ProfileManagement"; +import { Gui } from "@zowe/zowe-explorer-api"; +import { ZoweLogger } from "../../../src/utils/LoggerUtils"; +import { Profiles } from "../../../src/Profiles"; +import * as vscode from "vscode"; +import { imperative } from "@zowe/cli"; +import { ZoweUSSNode } from "../../../src/uss/ZoweUSSNode"; + +jest.mock("fs"); +jest.mock("vscode"); + +describe("ProfileManagement unit tests", () => { + afterEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + }); + function createGlobalMocks(): any { + const newMocks = { + mockSession: sharedMock.createISession(), + mockBasicAuthProfile: sharedMock.createValidIProfile(), + mockTokenAuthProfile: sharedMock.createTokenAuthIProfile(), + mockNoAuthProfile: sharedMock.createNoAuthIProfile(), + opCancelledSpy: jest.spyOn(Gui, "infoMessage"), + mockDsSessionNode: ZoweDatasetNode, + mockUnixSessionNode: ZoweUSSNode, + mockResolveQp: jest.fn(), + mockCreateQp: jest.fn(), + mockUpdateChosen: ProfileManagement.basicAuthUpdateQpItems[ProfileManagement.AuthQpLabels.update], + mockAddBasicChosen: ProfileManagement.basicAuthAddQpItems[ProfileManagement.AuthQpLabels.add], + mockLoginChosen: ProfileManagement.tokenAuthLoginQpItem[ProfileManagement.AuthQpLabels.login], + mockLogoutChosen: ProfileManagement.tokenAuthLogoutQpItem[ProfileManagement.AuthQpLabels.logout], + mockEditProfChosen: ProfileManagement.editProfileQpItems[ProfileManagement.AuthQpLabels.edit], + mockDeleteProfChosen: ProfileManagement.deleteProfileQpItem[ProfileManagement.AuthQpLabels.delete], + mockHideProfChosen: ProfileManagement.hideProfileQpItems[ProfileManagement.AuthQpLabels.hide], + mockEnableValidationChosen: ProfileManagement.enableProfileValildationQpItem[ProfileManagement.AuthQpLabels.enable], + mockDisableValidationChosen: ProfileManagement.disableProfileValildationQpItem[ProfileManagement.AuthQpLabels.disable], + mockProfileInfo: { usingTeamConfig: true }, + mockProfileInstance: null as any, + debugLogSpy: null as any, + promptSpy: null as any, + editSpy: null as any, + loginSpy: null as any, + logoutSpy: null as any, + logMsg: null as any, + commandSpy: null as any, + }; + Object.defineProperty(profUtils.ProfilesUtils, "promptCredentials", { value: jest.fn(), configurable: true }); + newMocks.promptSpy = jest.spyOn(profUtils.ProfilesUtils, "promptCredentials"); + Object.defineProperty(ZoweLogger, "debug", { value: jest.fn(), configurable: true }); + newMocks.debugLogSpy = jest.spyOn(ZoweLogger, "debug"); + Object.defineProperty(Gui, "resolveQuickPick", { value: newMocks.mockResolveQp, configurable: true }); + newMocks.mockCreateQp.mockReturnValue({ + show: jest.fn(() => { + return {}; + }), + hide: jest.fn(() => { + return {}; + }), + onDidAccept: jest.fn(() => { + return {}; + }), + }); + Object.defineProperty(Gui, "createQuickPick", { value: newMocks.mockCreateQp, configurable: true }); + newMocks.mockDsSessionNode = dsMock.createDatasetSessionNode(newMocks.mockSession, newMocks.mockBasicAuthProfile) as any; + newMocks.mockProfileInstance = sharedMock.createInstanceOfProfile(newMocks.mockBasicAuthProfile); + Object.defineProperty(Profiles, "getInstance", { + value: jest.fn().mockReturnValue(newMocks.mockProfileInstance), + configurable: true, + }); + Object.defineProperty(newMocks.mockProfileInstance, "editSession", { value: jest.fn(), configurable: true }); + 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, "ssoLogout", { value: jest.fn(), configurable: true }); + newMocks.logoutSpy = jest.spyOn(newMocks.mockProfileInstance, "ssoLogout"); + Object.defineProperty(vscode.commands, "executeCommand", { value: jest.fn(), configurable: true }); + newMocks.commandSpy = jest.spyOn(vscode.commands, "executeCommand"); + + return newMocks; + } + + describe("unit tests around basic auth selections", () => { + function createBlockMocks(globalMocks): any { + globalMocks.logMsg = `Profile ${globalMocks.mockBasicAuthProfile.name} is using basic authentication.`; + globalMocks.mockDsSessionNode.getProfile = jest.fn().mockReturnValue(globalMocks.mockBasicAuthProfile); + return globalMocks; + } + it("profile using basic authentication should see Operation Cancelled when escaping quick pick", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(undefined); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.opCancelledSpy).toBeCalledWith("Operation Cancelled"); + }); + it("profile using basic authentication should see promptCredentials called when Update Credentials chosen", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockUpdateChosen); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.promptSpy).toBeCalled(); + }); + it("profile using basic authentication should see editSession called when Edit Profile chosen", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockEditProfChosen); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.editSpy).toBeCalled(); + }); + it("profile using basic authentication should see editSession called when Delete Profile chosen with v2 profile", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + Object.defineProperty(mocks.mockProfileInstance, "getProfileInfo", { + value: jest.fn().mockResolvedValue(mocks.mockProfileInfo as imperative.ProfileInfo), + configurable: true, + }); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockDeleteProfChosen); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.editSpy).toBeCalled(); + }); + it("profile using basic authentication should see hide session command called for profile in data set tree view", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + Object.defineProperty(mocks.mockProfileInstance, "getProfileInfo", { + value: jest.fn().mockResolvedValue(mocks.mockProfileInfo as imperative.ProfileInfo), + configurable: true, + }); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockHideProfChosen); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.commandSpy).toHaveBeenLastCalledWith("zowe.ds.removeSession", mocks.mockDsSessionNode); + }); + it("profile using basic authentication should see delete commands called when Delete Profile chosen with v1 profile", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockDeleteProfChosen); + mocks.mockProfileInfo.usingTeamConfig = false; + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.editSpy).not.toBeCalled(); + expect(mocks.commandSpy).toHaveBeenLastCalledWith("zowe.ds.deleteProfile", mocks.mockDsSessionNode); + }); + }); + describe("unit tests around token auth selections", () => { + function createBlockMocks(globalMocks): any { + globalMocks.logMsg = `Profile ${globalMocks.mockTokenAuthProfile.name} is using token authentication.`; + globalMocks.mockUnixSessionNode = unixMock.createUSSSessionNode(globalMocks.mockSession, globalMocks.mockBasicAuthProfile) as any; + Object.defineProperty(profUtils.ProfilesUtils, "isUsingTokenAuth", { value: jest.fn().mockResolvedValueOnce(true), configurable: true }); + globalMocks.mockDsSessionNode.getProfile = jest.fn().mockReturnValue(globalMocks.mockTokenAuthProfile); + globalMocks.mockUnixSessionNode.getProfile = jest.fn().mockReturnValue(globalMocks.mockTokenAuthProfile); + return globalMocks; + } + it("profile using token authentication should see Operation Cancelled when escaping quick pick", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(undefined); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.opCancelledSpy).toBeCalledWith("Operation Cancelled"); + }); + it("profile using token authentication should see ssoLogin called when Log in to authentication service chosen", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockLoginChosen); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.loginSpy).toBeCalled(); + }); + it("profile using token authentication should see ssoLogout called when Log out from authentication service chosen", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockLogoutChosen); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.logoutSpy).toBeCalled(); + }); + it("profile using token authentication should see correct command called for hiding a unix tree session node", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockHideProfChosen); + await ProfileManagement.manageProfile(mocks.mockUnixSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.commandSpy).toHaveBeenLastCalledWith("zowe.uss.removeSession", mocks.mockUnixSessionNode); + }); + it("profile using token authentication should see correct command called for enabling validation a unix tree session node", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockEnableValidationChosen); + await ProfileManagement.manageProfile(mocks.mockUnixSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.commandSpy).toHaveBeenLastCalledWith("zowe.uss.enableValidation", mocks.mockUnixSessionNode); + }); + it("profile using token authentication should see correct command called for disabling validation a unix tree session node", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockDisableValidationChosen); + await ProfileManagement.manageProfile(mocks.mockUnixSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.commandSpy).toHaveBeenLastCalledWith("zowe.uss.disableValidation", mocks.mockUnixSessionNode); + }); + }); + describe("unit tests around no auth declared selections", () => { + function createBlockMocks(globalMocks): any { + globalMocks.logMsg = `Profile ${globalMocks.mockNoAuthProfile.name} authentication method is unkown.`; + Object.defineProperty(profUtils.ProfilesUtils, "isUsingTokenAuth", { value: jest.fn().mockResolvedValueOnce(false), configurable: true }); + globalMocks.mockDsSessionNode.getProfile = jest.fn().mockReturnValue(globalMocks.mockNoAuthProfile); + return globalMocks; + } + it("profile with no authentication method should see Operation Cancelled when escaping quick pick", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(undefined); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.opCancelledSpy).toBeCalledWith("Operation Cancelled"); + }); + it("profile with no authentication method should see promptCredentials called when Add Basic Credentials chosen", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockAddBasicChosen); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.promptSpy).toBeCalled(); + }); + it("profile with no authentication method should see ssoLogin called when Log in to authentication service chosen", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockLoginChosen); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.loginSpy).toBeCalled(); + }); + it("profile with no authentication method should see editSession called when Edit Profile chosen", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockEditProfChosen); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.editSpy).toBeCalled(); + }); + it("profile using token authentication should see correct command called for enabling validation a data set tree session node", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockEnableValidationChosen); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.commandSpy).toHaveBeenLastCalledWith("zowe.ds.enableValidation", mocks.mockDsSessionNode); + }); + it("profile using token authentication should see correct command called for disabling validation a data set tree session node", async () => { + const mocks = createBlockMocks(createGlobalMocks()); + mocks.mockResolveQp.mockResolvedValueOnce(mocks.mockDisableValidationChosen); + await ProfileManagement.manageProfile(mocks.mockDsSessionNode); + expect(mocks.debugLogSpy).toBeCalledWith(mocks.logMsg); + expect(mocks.commandSpy).toHaveBeenLastCalledWith("zowe.ds.disableValidation", mocks.mockDsSessionNode); + }); + }); +}); diff --git a/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts index 65a2121ff8..74cdb85a8a 100644 --- a/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts @@ -743,7 +743,7 @@ describe("ProfilesUtils unit tests", () => { jest.spyOn(Profiles.getInstance(), "getDefaultProfile").mockReturnValueOnce({} as any); jest.spyOn(Profiles.getInstance(), "getLoadedProfConfig").mockResolvedValue({ type: "test" } as any); jest.spyOn(Profiles.getInstance(), "getSecurePropsForProfile").mockResolvedValue([]); - await expect(profUtils.isUsingTokenAuth("test")).resolves.toEqual(false); + await expect(profUtils.ProfilesUtils.isUsingTokenAuth("test")).resolves.toEqual(false); }); }); }); diff --git a/packages/zowe-explorer/i18n/sample/package.i18n.json b/packages/zowe-explorer/i18n/sample/package.i18n.json index 8bc5d8bf6c..a73e7985e5 100644 --- a/packages/zowe-explorer/i18n/sample/package.i18n.json +++ b/packages/zowe-explorer/i18n/sample/package.i18n.json @@ -3,6 +3,7 @@ "description": "VS Code extension, powered by Zowe CLI, that streamlines interaction with mainframe data sets, USS files, and jobs", "viewsContainers.activitybar": "Zowe Explorer", "zowe.promptCredentials": "Update Credentials", + "zowe.profileManagement": "Manage Profile", "zowe.extRefresh": "Refresh Zowe Explorer", "zowe.ds.explorer": "Data Sets", "zowe.uss.explorer": "Unix System Services (USS)", diff --git a/packages/zowe-explorer/i18n/sample/src/Profiles.i18n.json b/packages/zowe-explorer/i18n/sample/src/Profiles.i18n.json index 8c54f161a0..a7bdfe81e4 100644 --- a/packages/zowe-explorer/i18n/sample/src/Profiles.i18n.json +++ b/packages/zowe-explorer/i18n/sample/src/Profiles.i18n.json @@ -1,5 +1,6 @@ { "profiles.operation.cancelled": "Operation Cancelled", + "profiles.manualEditMsg": "The Team configuration file has been opened in the editor. Editing or removal of profiles will need to be done manually.", "checkCurrentProfile.tokenAuthError.msg": "Token auth error", "checkCurrentProfile.tokenAuthError.additionalDetails": "Profile was found using token auth, please log in to continue.", "profiles.createNewConnection": "$(plus) Create a new connection to z/OS", @@ -26,11 +27,12 @@ "validateProfiles.progress": "Validating {0} Profile.", "validateProfiles.cancelled": "Validating {0} was cancelled.", "validateProfiles.error": "Profile validation failed for {0}.", - "ssoAuth.noBase": "This profile does not support token authentication.", - "ssoLogin.successful": "Login to authentication service was successful.", + "ssoAuth.usingBasicAuth": "This profile is using basic authentication and does not support token authentication.", + "ssoLogin.tokenType.error": "Error getting supported tokenType value for profile {0}", "ssoLogin.error": "Unable to log in with {0}. {1}", "ssoLogout.successful": "Logout from authentication service was successful for {0}.", "ssoLogout.error": "Unable to log out with {0}. {1}", + "ssoLogin.successful": "Login to authentication service was successful.", "getConfigLocationPrompt.placeholder.create": "Select the location where the config file will be initialized", "getConfigLocationPrompt.placeholder.edit": "Select the location of the config file to edit", "getConfigLocationPrompt.showQuickPick.global": "Global: in the Zowe home directory", diff --git a/packages/zowe-explorer/i18n/sample/src/utils/ProfileManagement.i18n.json b/packages/zowe-explorer/i18n/sample/src/utils/ProfileManagement.i18n.json new file mode 100644 index 0000000000..f487573b50 --- /dev/null +++ b/packages/zowe-explorer/i18n/sample/src/utils/ProfileManagement.i18n.json @@ -0,0 +1,23 @@ +{ + "profiles.operation.cancelled": "Operation Cancelled", + "qpPlaceholders.qp.basic": "Profile {0} is using basic authentication. Choose a profile action.", + "qpPlaceholders.qp.token": "Profile {0} is using token authentication. Choose a profile action.", + "qpPlaceholders.qp.choose": "Profile {0} doesn't specify an authentication method. Choose a profile action.", + "addBasicAuthQpItem.addCredentials.qpLabel": "$(plus) Add Credentials", + "addBasicAuthQpItem.addCredentials.qpDetail": "Add username and password for basic authentication", + "updateBasicAuthQpItem.updateCredentials.qpLabel": "$(refresh) Update Credentials", + "updateBasicAuthQpItem.updateCredentials.qpDetail": "Update stored username and password", + "deleteProfileQpItem.delete.qpLabel": "$(trash) Delete Profile", + "disableProfileValildationQpItem.disableValidation.qpLabel": "$(workspace-untrusted) Disable Profile Validation", + "disableProfileValildationQpItem.disableValidation.qpDetail": "Disable validation of server check for profile", + "enableProfileValildationQpItem.enableValidation.qpLabel": "$(workspace-trusted) Enable Profile Validation", + "enableProfileValildationQpItem.enableValidation.qpDetail": "Enable validation of server check for profile", + "editProfileQpItem.editProfile.qpLabel": "$(pencil) Edit Profile", + "editProfileQpItem.editProfile.qpDetail": "Update profile connection information", + "hideProfileQpItems.hideProfile.qpLabel": "$(eye-closed) Hide Profile", + "hideProfileQpItems.hideProfile.qpDetail": "Hide profile name from tree view", + "loginQpItem.login.qpLabel": "$(arrow-right) Log in to authentication service", + "loginQpItem.login.qpDetail": "Log in to obtain a new token value", + "logoutQpItem.logout.qpLabel": "$(arrow-left) Log out of authentication service", + "logoutQpItem.logout.qpDetail": "Log out to invalidate and remove stored token value" +} diff --git a/packages/zowe-explorer/package.json b/packages/zowe-explorer/package.json index 2c95e16902..c602c3c2d8 100644 --- a/packages/zowe-explorer/package.json +++ b/packages/zowe-explorer/package.json @@ -141,6 +141,11 @@ "title": "%zowe.promptCredentials%", "category": "Zowe Explorer" }, + { + "command": "zowe.profileManagement", + "title": "%zowe.profileManagement%", + "category": "Zowe Explorer" + }, { "command": "zowe.extRefresh", "title": "%zowe.extRefresh%", @@ -953,45 +958,10 @@ "command": "zowe.uss.deleteNode", "group": "099_zowe_ussModification:@4" }, - { - "when": "view == zowe.uss.explorer && viewItem =~ /_validate/ && !listMultiSelection", - "command": "zowe.uss.disableValidation", - "group": "098_zowe_ussProfileAuthentication@1" - }, - { - "when": "view == zowe.uss.explorer && viewItem =~ /_noValidate/ && !listMultiSelection", - "command": "zowe.uss.enableValidation", - "group": "098_zowe_ussProfileAuthentication@2" - }, - { - "when": "view == zowe.uss.explorer && viewItem =~ /^(?!.*_fav.*)ussSession.*/ && !listMultiSelection", - "command": "zowe.promptCredentials", - "group": "098_zowe_ussProfileAuthentication@3" - }, - { - "when": "view == zowe.uss.explorer && viewItem =~ /^(?!.*_fav.*)ussSession.*/ && !listMultiSelection", - "command": "zowe.uss.ssoLogin", - "group": "098_zowe_ussProfileAuthentication@4" - }, { "when": "view == zowe.uss.explorer && viewItem =~ /^(?!.*_fav.*)ussSession.*/ && !listMultiSelection", - "command": "zowe.uss.ssoLogout", - "group": "098_zowe_ussProfileAuthentication@5" - }, - { - "when": "viewItem =~ /^(?!.*_fav.*)ussSession.*/ && !listMultiSelection", - "command": "zowe.uss.editSession", - "group": "099_zowe_ussProfileModification@1" - }, - { - "when": "viewItem =~ /^(?!.*_fav.*)ussSession.*/", - "command": "zowe.uss.removeSession", - "group": "099_zowe_ussProfileModification@98" - }, - { - "when": "viewItem =~ /^(?!.*_fav.*)ussSession.*/ && !listMultiSelection", - "command": "zowe.uss.deleteProfile", - "group": "099_zowe_ussProfileModification@99" + "command": "zowe.profileManagement", + "group": "099_zowe_ussProfileAuthentication@99" }, { "when": "view == zowe.ds.explorer && viewItem =~ /^(?!.*_fav.*)session.*/ && !listMultiSelection", @@ -1168,45 +1138,10 @@ "command": "zowe.ds.deleteDataset", "group": "099_zowe_dsModification@5" }, - { - "when": "view == zowe.ds.explorer && viewItem =~ /_validate/ && !listMultiSelection", - "command": "zowe.ds.disableValidation", - "group": "098_zowe_dsProfileAuthentication@6" - }, - { - "when": "view == zowe.ds.explorer && viewItem =~ /_noValidate/ && !listMultiSelection", - "command": "zowe.ds.enableValidation", - "group": "098_zowe_dsProfileAuthentication@7" - }, - { - "when": "view == zowe.ds.explorer && viewItem =~ /^(?!.*_fav.*)session.*/ && !listMultiSelection", - "command": "zowe.promptCredentials", - "group": "098_zowe_dsProfileAuthentication@8" - }, - { - "when": "view == zowe.ds.explorer && viewItem =~ /^(?!.*_fav.*)session.*/ && !listMultiSelection", - "command": "zowe.ds.ssoLogin", - "group": "098_zowe_dsProfileAuthentication@9" - }, { "when": "view == zowe.ds.explorer && viewItem =~ /^(?!.*_fav.*)session.*/ && !listMultiSelection", - "command": "zowe.ds.ssoLogout", - "group": "098_zowe_dsProfileAuthentication@10" - }, - { - "when": "view == zowe.ds.explorer && viewItem =~ /^(?!.*_fav.*)session.*/ && !listMultiSelection", - "command": "zowe.ds.editSession", - "group": "099_zowe_dsProfileModification@0" - }, - { - "when": "view == zowe.ds.explorer && viewItem =~ /^(?!.*_fav.*)session.*/", - "command": "zowe.ds.removeSession", - "group": "099_zowe_dsProfileModification@98" - }, - { - "when": "view == zowe.ds.explorer && viewItem =~ /^(?!.*_fav.*)session.*/ && !listMultiSelection", - "command": "zowe.ds.deleteProfile", - "group": "099_zowe_dsProfileModification@99" + "command": "zowe.profileManagement", + "group": "099_zowe_dsProfileAuthentication@99" }, { "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/ && !listMultiSelection", @@ -1333,45 +1268,10 @@ "command": "zowe.jobs.cancelJob", "group": "099_zowe_jobsModification" }, - { - "when": "view == zowe.jobs.explorer && viewItem =~ /_validate/ && !listMultiSelection", - "command": "zowe.jobs.disableValidation", - "group": "098_zowe_jobsProfileAuthentication@3" - }, - { - "when": "view == zowe.jobs.explorer && viewItem =~ /_noValidate/ && !listMultiSelection", - "command": "zowe.jobs.enableValidation", - "group": "098_zowe_jobsProfileAuthentication@4" - }, - { - "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/ && !listMultiSelection", - "command": "zowe.promptCredentials", - "group": "098_zowe_jobsProfileAuthentication@5" - }, - { - "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/ && !listMultiSelection", - "command": "zowe.jobs.ssoLogin", - "group": "098_zowe_jobsProfileAuthentication@6" - }, { "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/ && !listMultiSelection", - "command": "zowe.jobs.ssoLogout", - "group": "098_zowe_jobsProfileAuthentication@7" - }, - { - "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/ && !listMultiSelection", - "command": "zowe.jobs.editSession", - "group": "099_zowe_jobsProfileModification@0" - }, - { - "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/", - "command": "zowe.jobs.removeJobsSession", - "group": "099_zowe_jobsProfileModification@98" - }, - { - "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/ && !listMultiSelection", - "command": "zowe.jobs.deleteProfile", - "group": "099_zowe_jobsProfileModification@99" + "command": "zowe.profileManagement", + "group": "099_zowe_jobsProfileAuthentication@99" }, { "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/ && !listMultiSelection", @@ -1729,6 +1629,10 @@ { "command": "zowe.jobs.deleteJob", "when": "never" + }, + { + "command": "zowe.profileManagement", + "when": "never" } ] }, diff --git a/packages/zowe-explorer/package.nls.json b/packages/zowe-explorer/package.nls.json index 8bc5d8bf6c..a73e7985e5 100644 --- a/packages/zowe-explorer/package.nls.json +++ b/packages/zowe-explorer/package.nls.json @@ -3,6 +3,7 @@ "description": "VS Code extension, powered by Zowe CLI, that streamlines interaction with mainframe data sets, USS files, and jobs", "viewsContainers.activitybar": "Zowe Explorer", "zowe.promptCredentials": "Update Credentials", + "zowe.profileManagement": "Manage Profile", "zowe.extRefresh": "Refresh Zowe Explorer", "zowe.ds.explorer": "Data Sets", "zowe.uss.explorer": "Unix System Services (USS)", diff --git a/packages/zowe-explorer/src/Profiles.ts b/packages/zowe-explorer/src/Profiles.ts index 29820ac9b0..ba164cd8d6 100644 --- a/packages/zowe-explorer/src/Profiles.ts +++ b/packages/zowe-explorer/src/Profiles.ts @@ -30,7 +30,7 @@ import { getFullPath, getZoweDir, } from "@zowe/zowe-explorer-api"; -import { errorHandling, FilterDescriptor, FilterItem, ProfilesUtils, isUsingTokenAuth } from "./utils/ProfilesUtils"; +import { errorHandling, FilterDescriptor, FilterItem, ProfilesUtils } from "./utils/ProfilesUtils"; import { ZoweExplorerApiRegister } from "./ZoweExplorerApiRegister"; import { ZoweExplorerExtender } from "./ZoweExplorerExtender"; import * as globals from "./globals"; @@ -68,6 +68,10 @@ export class Profiles extends ProfilesCache { private jobsSchema: string = globals.SETTINGS_JOBS_HISTORY; private mProfileInfo: zowe.imperative.ProfileInfo; private profilesOpCancelled = localize("profiles.operation.cancelled", "Operation Cancelled"); + private manualEditMsg = localize( + "profiles.manualEditMsg", + "The Team configuration file has been opened in the editor. Editing or removal of profiles will need to be done manually." + ); public constructor(log: zowe.imperative.Logger, cwd?: string) { super(log, cwd); } @@ -88,7 +92,7 @@ export class Profiles extends ProfilesCache { public async checkCurrentProfile(theProfile: zowe.imperative.IProfileLoaded): Promise { ZoweLogger.trace("Profiles.checkCurrentProfile called."); let profileStatus: IProfileValidation; - const usingTokenAuth = await isUsingTokenAuth(theProfile.name); + const usingTokenAuth = await ProfilesUtils.isUsingTokenAuth(theProfile.name); if (usingTokenAuth && !theProfile.profile.tokenType) { const error = new zowe.imperative.ImperativeError({ @@ -443,6 +447,7 @@ export class Profiles extends ProfilesCache { const currentProfile = await this.getProfileFromConfig(profileLoaded.name); const filePath = currentProfile.profLoc.osLoc[0]; await this.openConfigFile(filePath); + Gui.showMessage(this.manualEditMsg); return; } const editSession = this.loadNamedProfile(profileLoaded.name, profileLoaded.type).profile; @@ -689,6 +694,7 @@ export class Profiles extends ProfilesCache { const existingLayers = await this.getConfigLayers(); if (existingLayers.length === 1) { await this.openConfigFile(existingLayers[0].path); + Gui.showMessage(this.manualEditMsg); } if (existingLayers && existingLayers.length > 1) { const choice = await this.getConfigLocationPrompt("edit"); @@ -699,6 +705,7 @@ export class Profiles extends ProfilesCache { await this.openConfigFile(file.path); } } + Gui.showMessage(this.manualEditMsg); break; case "global": for (const file of existingLayers) { @@ -706,12 +713,12 @@ export class Profiles extends ProfilesCache { await this.openConfigFile(file.path); } } + Gui.showMessage(this.manualEditMsg); break; default: Gui.showMessage(this.profilesOpCancelled); - return; + break; } - return; } } @@ -1167,8 +1174,10 @@ export class Profiles extends ProfilesCache { serviceProfile = this.loadNamedProfile(label.trim()); } // This check will handle service profiles that have username and password - if (serviceProfile.profile.user && serviceProfile.profile.password) { - Gui.showMessage(localize("ssoAuth.noBase", "This profile does not support token authentication.")); + if (ProfilesUtils.isProfileUsingBasicAuth(serviceProfile)) { + Gui.showMessage( + localize("ssoAuth.usingBasicAuth", "This profile is using basic authentication and does not support token authentication.") + ); return; } @@ -1176,7 +1185,7 @@ export class Profiles extends ProfilesCache { loginTokenType = await ZoweExplorerApiRegister.getInstance().getCommonApi(serviceProfile).getTokenTypeName(); } catch (error) { ZoweLogger.warn(error); - Gui.showMessage(localize("ssoAuth.noBase", "This profile does not support token authentication.")); + Gui.showMessage(localize("ssoLogin.tokenType.error", "Error getting supported tokenType value for profile {0}", serviceProfile.name)); return; } try { @@ -1185,7 +1194,6 @@ export class Profiles extends ProfilesCache { } else { await this.loginWithBaseProfile(serviceProfile, loginTokenType, node); } - Gui.showMessage(localize("ssoLogin.successful", "Login to authentication service was successful.")); } catch (err) { const message = localize("ssoLogin.error", "Unable to log in with {0}. {1}", serviceProfile.name, err?.message); ZoweLogger.error(message); @@ -1198,8 +1206,10 @@ export class Profiles extends ProfilesCache { ZoweLogger.trace("Profiles.ssoLogout called."); const serviceProfile = node.getProfile(); // This check will handle service profiles that have username and password - if (serviceProfile.profile?.user && serviceProfile.profile?.password) { - Gui.showMessage(localize("ssoAuth.noBase", "This profile does not support token authentication.")); + if (ProfilesUtils.isProfileUsingBasicAuth(serviceProfile)) { + Gui.showMessage( + localize("ssoAuth.usingBasicAuth", "This profile is using basic authentication and does not support token authentication.") + ); return; } try { @@ -1288,6 +1298,7 @@ export class Profiles extends ProfilesCache { profile: { ...node.getProfile().profile, ...updBaseProfile }, }); } + Gui.showMessage(localize("ssoLogin.successful", "Login to authentication service was successful.")); } } @@ -1313,6 +1324,7 @@ export class Profiles extends ProfilesCache { profile: { ...node.getProfile().profile, ...session }, }); } + Gui.showMessage(localize("ssoLogin.successful", "Login to authentication service was successful.")); } private async getConfigLocationPrompt(action: string): Promise { diff --git a/packages/zowe-explorer/src/globals.ts b/packages/zowe-explorer/src/globals.ts index a503fe7332..3d3d338c62 100644 --- a/packages/zowe-explorer/src/globals.ts +++ b/packages/zowe-explorer/src/globals.ts @@ -35,7 +35,7 @@ export let DS_DIR: string; export let CONFIG_PATH; // set during activate export let ISTHEIA = false; // set during activate export let LOG: imperative.Logger; -export const COMMAND_COUNT = 113; +export const COMMAND_COUNT = 114; export const MAX_SEARCH_HISTORY = 5; export const MAX_FILE_HISTORY = 10; export const MS_PER_SEC = 1000; diff --git a/packages/zowe-explorer/src/shared/init.ts b/packages/zowe-explorer/src/shared/init.ts index d023a1a969..67fc00fb3b 100644 --- a/packages/zowe-explorer/src/shared/init.ts +++ b/packages/zowe-explorer/src/shared/init.ts @@ -27,6 +27,7 @@ import { ZoweLogger } from "../utils/LoggerUtils"; import { ZoweSaveQueue } from "../abstract/ZoweSaveQueue"; import { SettingsConfig } from "../utils/SettingsConfig"; import { spoolFilePollEvent } from "../job/actions"; +import { ProfileManagement } from "../utils/ProfileManagement"; // Set up localization nls.config({ @@ -92,6 +93,12 @@ export function registerCommonCommands(context: vscode.ExtensionContext, provide }) ); + context.subscriptions.push( + vscode.commands.registerCommand("zowe.profileManagement", async (node: IZoweTreeNode) => { + await ProfileManagement.manageProfile(node); + }) + ); + // Register functions & event listeners context.subscriptions.push( vscode.workspace.onDidChangeConfiguration(async (e) => { diff --git a/packages/zowe-explorer/src/utils/ProfileManagement.ts b/packages/zowe-explorer/src/utils/ProfileManagement.ts new file mode 100644 index 0000000000..5825a8fdac --- /dev/null +++ b/packages/zowe-explorer/src/utils/ProfileManagement.ts @@ -0,0 +1,277 @@ +/** + * 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 vscode from "vscode"; +import * as globals from "../globals"; +import { Gui, IZoweTreeNode, imperative } from "@zowe/zowe-explorer-api"; +import { ZoweLogger } from "./LoggerUtils"; +import { ProfilesUtils } from "./ProfilesUtils"; +import * as nls from "vscode-nls"; +import { Profiles } from "../Profiles"; +import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; +import { isZoweDatasetTreeNode, isZoweUSSTreeNode } from "../shared/utils"; + +// Set up localization +nls.config({ + messageFormat: nls.MessageFormat.bundle, + bundleFormat: nls.BundleFormat.standalone, +})(); +const localize: nls.LocalizeFunc = nls.loadMessageBundle(); + +export class ProfileManagement { + public static async manageProfile(node: IZoweTreeNode): Promise { + const profile = node.getProfile(); + let selected: vscode.QuickPickItem; + switch (true) { + case ProfilesUtils.isProfileUsingBasicAuth(profile): { + ZoweLogger.debug(`Profile ${profile.name} is using basic authentication.`); + selected = await this.setupProfileManagementQp(imperative.SessConstants.AUTH_TYPE_BASIC, node); + break; + } + case await ProfilesUtils.isUsingTokenAuth(profile.name): { + ZoweLogger.debug(`Profile ${profile.name} is using token authentication.`); + selected = await this.setupProfileManagementQp("token", node); + break; + } + // will need a case for isUsingCertAuth + default: { + ZoweLogger.debug(`Profile ${profile.name} authentication method is unkown.`); + selected = await this.setupProfileManagementQp(null, node); + break; + } + } + await this.handleAuthSelection(selected, node, profile); + } + public static AuthQpLabels = { + add: "add-credentials", + delete: "delete-profile", + disable: "disable-validation", + edit: "edit-profile", + enable: "enable-validation", + hide: "hide-profile", + login: "obtain-token", + logout: "invalidate-token", + update: "update-credentials", + }; + public static basicAuthAddQpItems: Record = { + [this.AuthQpLabels.add]: { + label: localize("addBasicAuthQpItem.addCredentials.qpLabel", "$(plus) Add Credentials"), + description: localize("addBasicAuthQpItem.addCredentials.qpDetail", "Add username and password for basic authentication"), + }, + }; + public static basicAuthUpdateQpItems: Record = { + [this.AuthQpLabels.update]: { + label: localize("updateBasicAuthQpItem.updateCredentials.qpLabel", "$(refresh) Update Credentials"), + description: localize("updateBasicAuthQpItem.updateCredentials.qpDetail", "Update stored username and password"), + }, + }; + public static deleteProfileQpItem: Record = { + [this.AuthQpLabels.delete]: { + label: localize("deleteProfileQpItem.delete.qpLabel", "$(trash) Delete Profile"), + }, + }; + public static disableProfileValildationQpItem: Record = { + [this.AuthQpLabels.disable]: { + label: localize("disableProfileValildationQpItem.disableValidation.qpLabel", "$(workspace-untrusted) Disable Profile Validation"), + description: localize("disableProfileValildationQpItem.disableValidation.qpDetail", "Disable validation of server check for profile"), + }, + }; + public static enableProfileValildationQpItem: Record = { + [this.AuthQpLabels.enable]: { + label: localize("enableProfileValildationQpItem.enableValidation.qpLabel", "$(workspace-trusted) Enable Profile Validation"), + description: localize("enableProfileValildationQpItem.enableValidation.qpDetail", "Enable validation of server check for profile"), + }, + }; + public static editProfileQpItems: Record = { + [this.AuthQpLabels.edit]: { + label: localize("editProfileQpItem.editProfile.qpLabel", "$(pencil) Edit Profile"), + description: localize("editProfileQpItem.editProfile.qpDetail", "Update profile connection information"), + }, + }; + public static hideProfileQpItems: Record = { + [this.AuthQpLabels.hide]: { + label: localize("hideProfileQpItems.hideProfile.qpLabel", "$(eye-closed) Hide Profile"), + description: localize("hideProfileQpItems.hideProfile.qpDetail", "Hide profile name from tree view"), + }, + }; + public static tokenAuthLoginQpItem: Record = { + [this.AuthQpLabels.login]: { + label: localize("loginQpItem.login.qpLabel", "$(arrow-right) Log in to authentication service"), + description: localize("loginQpItem.login.qpDetail", "Log in to obtain a new token value"), + }, + }; + public static tokenAuthLogoutQpItem: Record = { + [this.AuthQpLabels.logout]: { + label: localize("logoutQpItem.logout.qpLabel", "$(arrow-left) Log out of authentication service"), + description: localize("logoutQpItem.logout.qpDetail", "Log out to invalidate and remove stored token value"), + }, + }; + private static async setupProfileManagementQp(managementType: string, node: IZoweTreeNode): Promise { + const profile = node.getProfile(); + const qp = Gui.createQuickPick(); + let quickPickOptions: vscode.QuickPickItem[]; + const placeholders = this.getQpPlaceholders(profile); + switch (managementType) { + case imperative.SessConstants.AUTH_TYPE_BASIC: { + quickPickOptions = this.basicAuthQp(node); + qp.placeholder = placeholders.basicAuth; + break; + } + case "token": { + quickPickOptions = this.tokenAuthQp(node); + qp.placeholder = placeholders.tokenAuth; + break; + } + default: { + quickPickOptions = this.chooseAuthQp(node); + qp.placeholder = placeholders.chooseAuth; + break; + } + } + let selectedItem = quickPickOptions[0]; + qp.items = quickPickOptions; + qp.activeItems = [selectedItem]; + qp.show(); + selectedItem = await Gui.resolveQuickPick(qp); + qp.hide(); + return selectedItem; + } + private static async handleAuthSelection(selected: vscode.QuickPickItem, node: IZoweTreeNode, profile: imperative.IProfileLoaded): Promise { + switch (selected) { + case this.basicAuthAddQpItems[this.AuthQpLabels.add]: { + await ProfilesUtils.promptCredentials(node); + break; + } + case this.editProfileQpItems[this.AuthQpLabels.edit]: { + await Profiles.getInstance().editSession(profile, profile.name); + break; + } + case this.tokenAuthLoginQpItem[this.AuthQpLabels.login]: { + await Profiles.getInstance().ssoLogin(node, profile.name); + break; + } + case this.tokenAuthLogoutQpItem[this.AuthQpLabels.logout]: { + await Profiles.getInstance().ssoLogout(node); + break; + } + case this.basicAuthUpdateQpItems[this.AuthQpLabels.update]: { + await ProfilesUtils.promptCredentials(node); + break; + } + case this.hideProfileQpItems[this.AuthQpLabels.hide]: { + await this.handleHideProfiles(node); + break; + } + case this.deleteProfileQpItem[this.AuthQpLabels.delete]: { + await this.handleDeleteProfiles(node); + break; + } + case this.enableProfileValildationQpItem[this.AuthQpLabels.enable]: { + await this.handleEnableProfileValidation(node); + break; + } + case this.disableProfileValildationQpItem[this.AuthQpLabels.disable]: { + await this.handleDisableProfileValidation(node); + break; + } + default: { + Gui.infoMessage(localize("profiles.operation.cancelled", "Operation Cancelled")); + break; + } + } + } + + private static getQpPlaceholders(profile: imperative.IProfileLoaded): { basicAuth: string; tokenAuth: string; chooseAuth: string } { + return { + basicAuth: localize("qpPlaceholders.qp.basic", "Profile {0} is using basic authentication. Choose a profile action.", profile.name), + tokenAuth: localize("qpPlaceholders.qp.token", "Profile {0} is using token authentication. Choose a profile action.", profile.name), + chooseAuth: localize( + "qpPlaceholders.qp.choose", + "Profile {0} doesn't specify an authentication method. Choose a profile action.", + profile.name + ), + }; + } + + private static basicAuthQp(node: IZoweTreeNode): vscode.QuickPickItem[] { + const quickPickOptions: vscode.QuickPickItem[] = Object.values(this.basicAuthUpdateQpItems); + return this.addFinalQpOptions(node, quickPickOptions); + } + private static tokenAuthQp(node: IZoweTreeNode): vscode.QuickPickItem[] { + const profile = node.getProfile(); + const quickPickOptions: vscode.QuickPickItem[] = Object.values(this.tokenAuthLoginQpItem); + if (profile.profile.tokenType) { + quickPickOptions.push(this.tokenAuthLogoutQpItem[this.AuthQpLabels.logout]); + } + return this.addFinalQpOptions(node, quickPickOptions); + } + private static chooseAuthQp(node: IZoweTreeNode): vscode.QuickPickItem[] { + const profile = node.getProfile(); + const quickPickOptions: vscode.QuickPickItem[] = Object.values(this.basicAuthAddQpItems); + try { + ZoweExplorerApiRegister.getInstance().getCommonApi(profile).getTokenTypeName(); + quickPickOptions.push(this.tokenAuthLoginQpItem[this.AuthQpLabels.login]); + } catch { + ZoweLogger.debug(`Profile ${profile.name} doesn't support token authentication, will not provide option.`); + } + return this.addFinalQpOptions(node, quickPickOptions); + } + private static addFinalQpOptions(node: IZoweTreeNode, quickPickOptions: vscode.QuickPickItem[]): vscode.QuickPickItem[] { + quickPickOptions.push(this.editProfileQpItems[this.AuthQpLabels.edit]); + quickPickOptions.push(this.hideProfileQpItems[this.AuthQpLabels.hide]); + if (node.contextValue.includes(globals.NO_VALIDATE_SUFFIX)) { + quickPickOptions.push(this.enableProfileValildationQpItem[this.AuthQpLabels.enable]); + } else { + quickPickOptions.push(this.disableProfileValildationQpItem[this.AuthQpLabels.disable]); + } + quickPickOptions.push(this.deleteProfileQpItem[this.AuthQpLabels.delete]); + return quickPickOptions; + } + private static async handleDeleteProfiles(node: IZoweTreeNode): Promise { + const profInfo = await Profiles.getInstance().getProfileInfo(); + if (profInfo.usingTeamConfig) { + const profile = node.getProfile(); + await Profiles.getInstance().editSession(profile, profile.name); + return; + } + await vscode.commands.executeCommand("zowe.ds.deleteProfile", node); + } + + private static async handleHideProfiles(node: IZoweTreeNode): Promise { + if (isZoweDatasetTreeNode(node)) { + return vscode.commands.executeCommand("zowe.ds.removeSession", node); + } + if (isZoweUSSTreeNode(node)) { + return vscode.commands.executeCommand("zowe.uss.removeSession", node); + } + return vscode.commands.executeCommand("zowe.jobs.removeJobsSession", node); + } + + private static async handleEnableProfileValidation(node: IZoweTreeNode): Promise { + if (isZoweDatasetTreeNode(node)) { + return vscode.commands.executeCommand("zowe.ds.enableValidation", node); + } + if (isZoweUSSTreeNode(node)) { + return vscode.commands.executeCommand("zowe.uss.enableValidation", node); + } + return vscode.commands.executeCommand("zowe.jobs.enableValidation", node); + } + + private static async handleDisableProfileValidation(node: IZoweTreeNode): Promise { + if (isZoweDatasetTreeNode(node)) { + return vscode.commands.executeCommand("zowe.ds.disableValidation", node); + } + if (isZoweUSSTreeNode(node)) { + return vscode.commands.executeCommand("zowe.uss.disableValidation", node); + } + return vscode.commands.executeCommand("zowe.jobs.disableValidation", node); + } +} diff --git a/packages/zowe-explorer/src/utils/ProfilesUtils.ts b/packages/zowe-explorer/src/utils/ProfilesUtils.ts index 612284de16..ec2b9d97e1 100644 --- a/packages/zowe-explorer/src/utils/ProfilesUtils.ts +++ b/packages/zowe-explorer/src/utils/ProfilesUtils.ts @@ -75,7 +75,7 @@ export async function errorHandling(errorDetails: Error | string, label?: string if (imperativeError.mDetails.additionalDetails) { const tokenError: string = imperativeError.mDetails.additionalDetails; - const isTokenAuth = await isUsingTokenAuth(label); + const isTokenAuth = await ProfilesUtils.isUsingTokenAuth(label); if (tokenError.includes("Token is not valid or expired.") || isTokenAuth) { if (isTheia()) { @@ -132,19 +132,6 @@ export function isTheia(): boolean { return false; } -/** - * Function that checks whether a profile is using token based authentication - * @param profileName the name of the profile to check - * @returns {Promise} a boolean representing whether token based auth is being used or not - */ -export async function isUsingTokenAuth(profileName: string): Promise { - const baseProfile = Profiles.getInstance().getDefaultProfile("base"); - const secureProfileProps = await Profiles.getInstance().getSecurePropsForProfile(profileName); - const secureBaseProfileProps = await Profiles.getInstance().getSecurePropsForProfile(baseProfile?.name); - const profileUsesBasicAuth = secureProfileProps.includes("user") && secureProfileProps.includes("password"); - return (secureProfileProps.includes("tokenValue") || secureBaseProfileProps.includes("tokenValue")) && !profileUsesBasicAuth; -} - /** * Function to update session and profile information in provided node * @param profiles is data source to find profiles @@ -320,6 +307,32 @@ export class ProfilesUtils { } } + /** + * Function that checks whether a profile is using basic authentication + * @param profile + * @returns {Promise} a boolean representing whether basic auth is being used or not + */ + public static isProfileUsingBasicAuth(profile: imperative.IProfileLoaded): boolean { + const prof = profile.profile; + return "user" in prof && "password" in prof; + } + + /** + * Function that checks whether a profile is using token based authentication + * @param profileName the name of the profile to check + * @returns {Promise} a boolean representing whether token based auth is being used or not + */ + public static async isUsingTokenAuth(profileName: string): Promise { + const secureProfileProps = await Profiles.getInstance().getSecurePropsForProfile(profileName); + const profileUsesBasicAuth = secureProfileProps.includes("user") && secureProfileProps.includes("password"); + if (secureProfileProps.includes("tokenValue")) { + return secureProfileProps.includes("tokenValue") && !profileUsesBasicAuth; + } + const baseProfile = Profiles.getInstance().getDefaultProfile("base"); + const secureBaseProfileProps = await Profiles.getInstance().getSecurePropsForProfile(baseProfile?.name); + return secureBaseProfileProps.includes("tokenValue") && !profileUsesBasicAuth; + } + public static async promptCredentials(node: IZoweTreeNode): Promise { ZoweLogger.trace("ProfilesUtils.promptCredentials called."); const mProfileInfo = await Profiles.getInstance().getProfileInfo();