Skip to content

Commit

Permalink
feat: Detect JWT expiration in token values and prompt user if expired (
Browse files Browse the repository at this point in the history
zowe#3174)

* feat: Detect JWT token expiration during validation

Signed-off-by: Trae Yelovich <[email protected]>

* refactor: Optimize logic in checkJwtTokenForProfile

Signed-off-by: Trae Yelovich <[email protected]>

* wip: resolve failing test cases

Signed-off-by: Trae Yelovich <[email protected]>

* tests: ZoweTreeProvider.checkJwtTokenForProfile test cases

Signed-off-by: Trae Yelovich <[email protected]>

* refactor: Remove old ESLint max-len comment

Signed-off-by: Trae Yelovich <[email protected]>

* fix(tests): failing test cases due to function change

Signed-off-by: Trae Yelovich <[email protected]>

* chore: run prepublish

Signed-off-by: Trae Yelovich <[email protected]>

* chore: update ZE changelog

Signed-off-by: Trae Yelovich <[email protected]>

* refactor: rename promptUserForTokenLogin -> promptUserForSsoLogin

Signed-off-by: Trae Yelovich <[email protected]>

* refactor: shorten logic in ZoweTreeProvider.checkJwtTokenForProfile

Signed-off-by: Trae Yelovich <[email protected]>

* update SDKs and checkJwtTokenForProfile function

Signed-off-by: Trae Yelovich <[email protected]>

* tests: Resolve failing tests after Imperative update

Signed-off-by: Trae Yelovich <[email protected]>

* chore: update ZE changelog to address feedback

Signed-off-by: Trae Yelovich <[email protected]>

* chore: update ZE API changelog

Signed-off-by: Trae Yelovich <[email protected]>

* chore: update lockfile

Signed-off-by: Trae Yelovich <[email protected]>

---------

Signed-off-by: Trae Yelovich <[email protected]>
Co-authored-by: Fernando Rijo Cedeno <[email protected]>
Co-authored-by: Timothy Johnson <[email protected]>
  • Loading branch information
3 people authored Oct 17, 2024
1 parent db7f988 commit cc16ff1
Show file tree
Hide file tree
Showing 14 changed files with 221 additions and 136 deletions.
1 change: 1 addition & 0 deletions packages/zowe-explorer-api/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t

- Zowe Explorer now includes support for the [VS Code display languages](https://code.visualstudio.com/docs/getstarted/locales) French, German, Japanese, Portuguese, and Spanish.
- Localization of strings within the webviews. [#2983](https://github.com/zowe/zowe-explorer-vscode/issues/2983)
- Update Zowe SDKs to `8.2.0` to get the latest enhancements from Imperative.

### Bug fixes

Expand Down
18 changes: 9 additions & 9 deletions packages/zowe-explorer-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@
},
"dependencies": {
"@types/vscode": "^1.53.2",
"@zowe/core-for-zowe-sdk": "^8.1.1",
"@zowe/imperative": "^8.1.0",
"@zowe/secrets-for-zowe-sdk": "^8.1.0",
"@zowe/zos-console-for-zowe-sdk": "^8.1.1",
"@zowe/zos-files-for-zowe-sdk": "^8.1.1",
"@zowe/zos-jobs-for-zowe-sdk": "^8.1.1",
"@zowe/zos-tso-for-zowe-sdk": "^8.1.1",
"@zowe/zos-uss-for-zowe-sdk": "^8.1.1",
"@zowe/zosmf-for-zowe-sdk": "^8.1.1",
"@zowe/core-for-zowe-sdk": "^8.2.0",
"@zowe/imperative": "^8.2.0",
"@zowe/secrets-for-zowe-sdk": "^8.1.2",
"@zowe/zos-console-for-zowe-sdk": "^8.2.0",
"@zowe/zos-files-for-zowe-sdk": "^8.2.0",
"@zowe/zos-jobs-for-zowe-sdk": "^8.2.0",
"@zowe/zos-tso-for-zowe-sdk": "^8.2.0",
"@zowe/zos-uss-for-zowe-sdk": "^8.2.0",
"@zowe/zosmf-for-zowe-sdk": "^8.2.0",
"deep-object-diff": "^1.1.9",
"mustache": "^4.2.0",
"semver": "^7.6.0"
Expand Down
4 changes: 2 additions & 2 deletions packages/zowe-explorer-ftp-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@
"vscode": "^1.79.0"
},
"dependencies": {
"@zowe/zos-files-for-zowe-sdk": "^8.1.1",
"@zowe/zos-files-for-zowe-sdk": "^8.2.0",
"@zowe/zos-ftp-for-zowe-cli": "^3.0.0",
"@zowe/zos-jobs-for-zowe-sdk": "^8.1.1",
"@zowe/zos-jobs-for-zowe-sdk": "^8.2.0",
"@zowe/zowe-explorer-api": "3.1.0-SNAPSHOT",
"tmp": "0.2.3"
},
Expand Down
1 change: 1 addition & 0 deletions packages/zowe-explorer/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen

- Zowe Explorer now includes support for the [VS Code display languages](https://code.visualstudio.com/docs/getstarted/locales) French, German, Japanese, Portuguese, and Spanish. Download the respective language pack and switch.
- Localization of strings within the webviews. [#2983](https://github.com/zowe/zowe-explorer-vscode/issues/2983)
- Added expired JSON web token detection for profiles in each tree view (Data Sets, USS, Jobs). When a user performs a search on a profile, they are prompted to log in if their token expired. [#3175](https://github.com/zowe/zowe-explorer-vscode/issues/3175)

### Bug fixes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ export function createInstanceOfProfileInfo() {
profLoc: { locType: 0, osLoc: ["location"], jsonLoc: "jsonLoc" },
},
],
hasTokenExpiredForProfile: jest.fn(),
updateProperty: jest.fn(),
updateKnownProperty: jest.fn(),
createSession: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { JobInit } from "../../../src/trees/job/JobInit";
import { createIJobObject, createJobSessionNode } from "../../__mocks__/mockCreators/jobs";
import { createDatasetSessionNode } from "../../__mocks__/mockCreators/datasets";
import { DatasetInit } from "../../../src/trees/dataset/DatasetInit";
import { AuthUtils } from "../../../src/utils/AuthUtils";

async function createGlobalMocks() {
Object.defineProperty(ZoweLocalStorage, "storage", {
Expand Down Expand Up @@ -309,6 +310,7 @@ describe("ZoweJobNode unit tests - Function checkCurrentProfile", () => {
testIJob: createIJobObject(),
testJobsProvider: await JobInit.createJobsTree(imperative.Logger.getAppLogger()),
jobNode: null,
checkJwtTokenForProfile: jest.spyOn(ZoweTreeProvider as any, "checkJwtTokenForProfile").mockImplementationOnce(() => {}),
};

newMocks.jobNode = new ZoweJobNode({
Expand Down Expand Up @@ -627,3 +629,53 @@ describe("Tree Provider Unit Tests - function isGlobalProfileNode", () => {
getOsLocInfoMock.mockRestore();
});
});

describe("Tree Provider Unit Tests - function checkJwtTokenForProfile", () => {
function getBlockMocks() {
const getAllProfiles = jest.fn().mockReturnValue([
{
profName: "zosmf",
profType: "zosmf",
isDefaultProfile: false,
profLoc: {
locType: imperative.ProfLocType.TEAM_CONFIG,
osLoc: ["/a/b/c/zowe.config.json"],
jsonLoc: ["profiles.zosmf"],
},
},
]);
const hasTokenExpiredForProfile = jest.fn();
const mergeArgsForProfile = jest.fn();
const profilesGetInstance = jest.spyOn(Profiles, "getInstance").mockReturnValue({
getProfileInfo: jest.fn().mockResolvedValue({
hasTokenExpiredForProfile,
getAllProfiles,
mergeArgsForProfile,
} as any),
} as any);

return {
getAllProfiles,
hasTokenExpiredForProfile,
mergeArgsForProfile,
profilesGetInstance,
};
}

it("returns early if the profile's token has not expired", async () => {
const blockMocks = getBlockMocks();
blockMocks.hasTokenExpiredForProfile.mockReturnValueOnce(false);
blockMocks.mergeArgsForProfile.mockReturnValue({ knownArgs: [{ argName: "tokenType", argValue: "LtpaToken2" }] });
await (ZoweTreeProvider as any).checkJwtTokenForProfile("zosmf");
expect(blockMocks.hasTokenExpiredForProfile).toHaveBeenCalledWith("zosmf");
});

it("prompts the user to log in if a JWT token is present and has expired", async () => {
const blockMocks = getBlockMocks();
blockMocks.hasTokenExpiredForProfile.mockReturnValueOnce(true);
const promptUserForSsoLogin = jest.spyOn(AuthUtils, "promptUserForSsoLogin").mockImplementation();
await (ZoweTreeProvider as any).checkJwtTokenForProfile("zosmf");
expect(blockMocks.hasTokenExpiredForProfile).toHaveBeenCalledWith("zosmf");
expect(promptUserForSsoLogin).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import { ZoweScheme } from "../../../../../zowe-explorer-api/src/fs/types/abstra
import { Sorting } from "../../../../../zowe-explorer-api/src/tree";
import { IconUtils } from "../../../../src/icons/IconUtils";
import { SharedContext } from "../../../../src/trees/shared/SharedContext";
import { ZoweTreeProvider } from "../../../../src/trees/ZoweTreeProvider";

jest.mock("fs");
jest.mock("util");
Expand Down Expand Up @@ -1495,6 +1496,7 @@ describe("Dataset Tree Unit Tests - Function datasetFilterPrompt", () => {
qpPlaceholder: 'Choose "Create new..." to define a new profile or select an existing profile to add to the Data Set Explorer',
mockEnableValidationContext: jest.fn(),
testTree: new DatasetTree(),
checkJwtTokenForProfile: jest.spyOn(ZoweTreeProvider as any, "checkJwtTokenForProfile").mockImplementationOnce(() => {}),
};

newMocks.datasetSessionNode = createDatasetSessionNode(newMocks.session, newMocks.imperativeProfile);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { IconUtils } from "../../../../src/icons/IconUtils";
import { FilterDescriptor } from "../../../../src/management/FilterManagement";
import { AuthUtils } from "../../../../src/utils/AuthUtils";
import { Icon } from "../../../../src/icons/Icon";
import { ZoweTreeProvider } from "../../../../src/trees/ZoweTreeProvider";

function createGlobalMocks() {
const globalMocks = {
Expand Down Expand Up @@ -550,6 +551,7 @@ describe("USSTree Unit Tests - Function filterPrompt", () => {
qpValue: "",
qpItem: new FilterDescriptor("\uFF0B " + "Create a new filter"),
resolveQuickPickHelper: jest.spyOn(Gui, "resolveQuickPick"),
checkJwtTokenForProfile: jest.spyOn(ZoweTreeProvider as any, "checkJwtTokenForProfile").mockImplementationOnce(() => {}),
};
newMocks.resolveQuickPickHelper.mockImplementation(() => Promise.resolve(newMocks.qpItem));
globalMocks.createQuickPick.mockReturnValue({
Expand Down Expand Up @@ -655,7 +657,7 @@ describe("USSTree Unit Tests - Function filterPrompt", () => {

it("Tests that filter() works correctly for favorited search nodes with credentials", async () => {
const globalMocks = createGlobalMocks();
await createBlockMocks(globalMocks);
const blockMocks = await createBlockMocks(globalMocks);

const sessionWithCred = createISession();
globalMocks.createSessCfgFromArgs.mockReturnValue(sessionWithCred);
Expand All @@ -669,6 +671,7 @@ describe("USSTree Unit Tests - Function filterPrompt", () => {
node.fullPath = "/u/myFolder";
globalMocks.testTree.mFavorites.push(node);
await expect(globalMocks.testTree.filterPrompt(node)).resolves.not.toThrow();
expect(blockMocks.checkJwtTokenForProfile).toHaveBeenCalledWith("ussTestSess2");
});

it("Tests that filter() works correctly for favorited search nodes without credentials", async () => {
Expand Down
58 changes: 29 additions & 29 deletions packages/zowe-explorer/l10n/bundle.l10n.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,13 @@
"Label"
]
},
"Your connection is no longer active for profile '{0}'. Please log in to an authentication service to restore the connection./Label": {
"Update Credentials": "Update Credentials",
"Your connection is no longer active for profile '{0}'. Please log in to an authentication service to restore the connection./Profile name": {
"message": "Your connection is no longer active for profile '{0}'. Please log in to an authentication service to restore the connection.",
"comment": [
"Label"
"Profile name"
]
},
"Update Credentials": "Update Credentials",
"Profile Name {0} is inactive. Please check if your Zowe server is active or if the URL and port in your profile is correct./Profile name": {
"message": "Profile Name {0} is inactive. Please check if your Zowe server is active or if the URL and port in your profile is correct.",
"comment": [
Expand Down Expand Up @@ -209,32 +209,6 @@
"Profile auth error": "Profile auth error",
"Profile is not authenticated, please log in to continue": "Profile is not authenticated, please log in to continue",
"Retrieving response from USS list API": "Retrieving response from USS list API",
"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.",
"Saving USS file...": "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"
]
},
"No error details given": "No error details given",
"Error fetching destination {0} for paste action: {1}/USS pathError message": {
"message": "Error fetching destination {0} for paste action: {1}",
"comment": [
"USS path",
"Error message"
]
},
"Downloaded: {0}/Download time": {
"message": "Downloaded: {0}",
"comment": [
Expand Down Expand Up @@ -305,6 +279,32 @@
"initializeUSSFavorites.error.buttonRemove": "initializeUSSFavorites.error.buttonRemove",
"File does not exist. It may have been deleted.": "File does not exist. It may have been deleted.",
"Pulling from Mainframe...": "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.",
"Saving USS file...": "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"
]
},
"No error details given": "No error details given",
"Error fetching destination {0} for paste action: {1}/USS pathError message": {
"message": "Error fetching destination {0} for paste action: {1}",
"comment": [
"USS path",
"Error message"
]
},
"{0} location/Node type": {
"message": "{0} location",
"comment": [
Expand Down
18 changes: 9 additions & 9 deletions packages/zowe-explorer/l10n/poeditor.json
Original file line number Diff line number Diff line change
Expand Up @@ -511,8 +511,8 @@
"Select Certificate Key": "",
"Required parameter 'host' must not be blank.": "",
"Invalid Credentials for profile '{0}'. Please ensure the username and password are valid or this may lead to a lock-out.": "",
"Your connection is no longer active for profile '{0}'. Please log in to an authentication service to restore the connection.": "",
"Update Credentials": "",
"Your connection is no longer active for profile '{0}'. Please log in to an authentication service to restore the connection.": "",
"Profile Name {0} is inactive. Please check if your Zowe server is active or if the URL and port in your profile is correct.": "",
"Use the search button to list USS files": "",
"Invalid node": "",
Expand All @@ -529,14 +529,6 @@
"Profile auth error": "",
"Profile is not authenticated, please log in to continue": "",
"Retrieving response from USS list API": "",
"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.": "",
"Saving USS file...": "",
"Renaming {0} failed due to API error: {1}": "",
"Deleting {0} failed due to API error: {1}": "",
"No error details given": "",
"Error fetching destination {0} for paste action: {1}": "",
"Downloaded: {0}": "",
"Encoding: {0}": "",
"Binary": "",
Expand Down Expand Up @@ -565,6 +557,14 @@
"initializeUSSFavorites.error.buttonRemove": "",
"File does not exist. It may have been deleted.": "",
"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.": "",
"Saving USS file...": "",
"Renaming {0} failed due to API error: {1}": "",
"Deleting {0} failed due to API error: {1}": "",
"No error details given": "",
"Error fetching destination {0} for paste action: {1}": "",
"{0} location": "",
"Choose a location to create the {0}": "",
"Name of file or directory": "",
Expand Down
10 changes: 5 additions & 5 deletions packages/zowe-explorer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1830,11 +1830,11 @@
},
"dependencies": {
"@vscode/codicons": "^0.0.35",
"@zowe/core-for-zowe-sdk": "^8.1.1",
"@zowe/secrets-for-zowe-sdk": "^8.1.0",
"@zowe/zos-files-for-zowe-sdk": "^8.1.1",
"@zowe/zos-jobs-for-zowe-sdk": "^8.1.1",
"@zowe/zosmf-for-zowe-sdk": "^8.1.1",
"@zowe/core-for-zowe-sdk": "^8.2.0",
"@zowe/secrets-for-zowe-sdk": "^8.1.2",
"@zowe/zos-files-for-zowe-sdk": "^8.2.0",
"@zowe/zos-jobs-for-zowe-sdk": "^8.2.0",
"@zowe/zosmf-for-zowe-sdk": "^8.2.0",
"@zowe/zowe-explorer-api": "3.1.0-SNAPSHOT",
"dayjs": "^1.11.10",
"fs-extra": "8.0.1",
Expand Down
16 changes: 16 additions & 0 deletions packages/zowe-explorer/src/trees/ZoweTreeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ export class ZoweTreeProvider<T extends IZoweTreeNode> {
Profiles.getInstance().validProfile = Validation.ValidationType.UNVERIFIED;
}
}
await ZoweTreeProvider.checkJwtTokenForProfile(node.getProfileName());
this.refresh();
return profileStatus;
}
Expand Down Expand Up @@ -304,6 +305,21 @@ export class ZoweTreeProvider<T extends IZoweTreeNode> {
await Profiles.getInstance().createZoweSession(zoweFileProvider);
}

/**
* Checks if a JWT token is used for authenticating the given profile name.
* If so, it will grab and decode the token to determine its expire date.
* If the token has expired, it will prompt the user to log in again.
*
* @param profileName The name of the profile to check the JWT token for
*/
protected static async checkJwtTokenForProfile(profileName: string): Promise<void> {
const profInfo = await Profiles.getInstance().getProfileInfo();

if (profInfo.hasTokenExpiredForProfile(profileName)) {
await AuthUtils.promptUserForSsoLogin(profileName);
}
}

private async loadProfileBySessionName(
sessionName: string,
treeProvider: IZoweTree<IZoweTreeNode>,
Expand Down
Loading

0 comments on commit cc16ff1

Please sign in to comment.