Skip to content

Commit

Permalink
wip: AuthHandler
Browse files Browse the repository at this point in the history
Signed-off-by: Trae Yelovich <[email protected]>
  • Loading branch information
traeok committed Dec 18, 2024
1 parent acf538c commit aa5251c
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 52 deletions.
156 changes: 156 additions & 0 deletions packages/zowe-explorer-api/src/profiles/AuthHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/**
* 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 { Gui } from "../globals";
import { CorrelatedError } from "../utils";
import * as imperative from "@zowe/imperative";
import { DeferredPromise } from "../utils";
import { IZoweTreeNode } from "../tree";

export interface IAuthMethods {
ssoLogin: (node?: IZoweTreeNode, profileName?: string) => PromiseLike<boolean>;
promptCredentials: (profile: string | imperative.IProfileLoaded, rePrompt?: boolean) => PromiseLike<string[]>;
}

export interface AuthPromptOpts extends IAuthMethods {
isUsingTokenAuth?: boolean;
errorCorrelation?: CorrelatedError;
}

type ProfileLike = string | imperative.IProfileLoaded;
export class AuthHandler {
private static lockedProfiles: Map<string, DeferredPromise<void>> = new Map();

/**
* Function that checks whether a profile is using token based authentication
* @param {string[]} secureProfileProps Secure properties for the service profile
* @param {string[]} baseSecureProfileProps Base profile's secure properties (optional)
* @returns {Promise<boolean>} a boolean representing whether token based auth is being used or not
*/
public static async isUsingTokenAuth(secureProfileProps: string[], baseSecureProfileProps?: string[]): Promise<boolean> {
const profileUsesBasicAuth = secureProfileProps.includes("user") && secureProfileProps.includes("password");
if (secureProfileProps.includes("tokenValue")) {
return secureProfileProps.includes("tokenValue") && !profileUsesBasicAuth;
}
return baseSecureProfileProps?.includes("tokenValue") && !profileUsesBasicAuth;
}

public static async unlockProfile(profileName: string) {
const deferred = this.lockedProfiles.get(profileName);
if (deferred) {
deferred.resolve();
this.lockedProfiles.delete(profileName);
}
}

public static async promptForAuthentication(
imperativeError: imperative.ImperativeError,
profile: ProfileLike,
opts: AuthPromptOpts
): Promise<boolean> {
const profileName = typeof profile === "string" ? profile : profile.name;
if (imperativeError.mDetails.additionalDetails) {
const tokenError: string = imperativeError.mDetails.additionalDetails;
if (tokenError.includes("Token is not valid or expired.") || opts.isUsingTokenAuth) {
const message = "Log in to Authentication Service";
const userResp = await Gui.showMessage(opts.errorCorrelation?.message ?? imperativeError.message, {
items: [message],
vsCodeOpts: { modal: true },
});
if (userResp === message) {
if (await opts.ssoLogin(null, profileName)) {
// SSO login was successful, unlock profile
AuthHandler.unlockProfile(profileName);
return true;
}
}
return false;
}
}
const checkCredsButton = "Update Credentials";
const creds = await Gui.errorMessage(opts.errorCorrelation?.message ?? imperativeError.message, {
items: [checkCredsButton],
vsCodeOpts: { modal: true },
}).then(async (selection) => {
if (selection !== checkCredsButton) {
return;
}
return opts.promptCredentials(profile, true);
});

if (creds != null) {
// New creds were provided, unlock profile
AuthHandler.unlockProfile(profileName);
return true;
}
return false;
}

public static async lockProfile(profile: ProfileLike, imperativeError: imperative.ImperativeError, opts: AuthPromptOpts): Promise<void> {
const profileName = typeof profile === "string" ? profile : profile.name;
if (this.lockedProfiles.has(profileName)) {
return this.lockedProfiles.get(profileName)!.promise;
}
const deferred = new DeferredPromise<void>();
this.lockedProfiles.set(profileName, deferred);

// Prompt the user to re-authenticate
const credsEntered = await AuthHandler.promptForAuthentication(imperativeError, profile, opts);

// If the user failed to re-authenticate, reject the promise
// TODO: more manual testing
if (!credsEntered) {
deferred.reject();
}

return deferred.promise;
}

public static async isLocked(profile: ProfileLike) {
return this.lockedProfiles.has(typeof profile === "string" ? profile : profile.name);
}

public static async waitIfLocked(profile: ProfileLike): Promise<void> {
const deferred = this.lockedProfiles.get(typeof profile === "string" ? profile : profile.name);
if (deferred) {
await deferred.promise;
}
}
}

export function withCredentialManagement<T extends (...args: any[]) => any | PromiseLike<any>>(
authMethods: IAuthMethods,
profile: ProfileLike,
apiMethod: T
): T {
return async function (...args: any[]) {
await AuthHandler.waitIfLocked(profile);

try {
return await apiMethod(...args);
} catch (error) {
if (error instanceof imperative.ImperativeError) {
const imperativeError: imperative.ImperativeError = error as imperative.ImperativeError;
const httpErrorCode = Number(imperativeError.mDetails.errorCode);
if (
httpErrorCode === imperative.RestConstants.HTTP_STATUS_401 ||
imperativeError.message.includes("All configured authentication methods failed")
) {
await AuthHandler.lockProfile(profile, imperativeError, { ...authMethods });
return await apiMethod(...args);
} else {
throw error;
}
}
throw error;
}
} as T;
}
1 change: 1 addition & 0 deletions packages/zowe-explorer-api/src/profiles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*
*/

export * from "./AuthHandler";
export * from "./Validation";
export * from "./UserSettings";
export * from "./ProfilesCache";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem
} catch (error) {
//Response will error if the file is not found
//Callers of fetchDatasetAtUri() do not expect it to throw an error
AuthUtils.promptForAuthError(error, metadata.profile);
await AuthUtils.promptForAuthOnError(error, metadata.profile);
return null;
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/zowe-explorer/src/trees/job/JobFSProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ export class JobFSProvider extends BaseProvider implements vscode.FileSystemProv
bufBuilder.write(await jesApi.getSpoolContentById(jobEntry.job.jobname, jobEntry.job.jobid, spoolEntry.spool.id));
}
} catch (err) {
AuthUtils.promptForAuthError(err, spoolEntry.metadata.profile);
await AuthUtils.promptForAuthOnError(err, spoolEntry.metadata.profile);
throw err;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/zowe-explorer/src/trees/uss/UssFSProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv
if (err instanceof Error) {
ZoweLogger.error(err.message);
}
AuthUtils.promptForAuthError(err, metadata.profile);
await AuthUtils.promptForAuthOnError(err, metadata.profile);
return;
}

Expand Down
71 changes: 22 additions & 49 deletions packages/zowe-explorer/src/utils/AuthUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

import * as util from "util";
import * as vscode from "vscode";
import { imperative, Gui, MainframeInteraction, IZoweTreeNode, ErrorCorrelator, ZoweExplorerApiType, CorrelatedError } from "@zowe/zowe-explorer-api";
import { imperative, Gui, MainframeInteraction, IZoweTreeNode, ErrorCorrelator, ZoweExplorerApiType, AuthHandler } from "@zowe/zowe-explorer-api";
import { Constants } from "../configuration/Constants";
import { ZoweLogger } from "../tools/ZoweLogger";
import { SharedTreeProviders } from "../trees/shared/SharedTreeProviders";
Expand All @@ -24,52 +24,24 @@ interface ErrorContext {
}

export class AuthUtils {
public static async promptForAuthentication(
imperativeError: imperative.ImperativeError,
profile: imperative.IProfileLoaded,
correlation?: CorrelatedError
): Promise<boolean> {
if (imperativeError.mDetails.additionalDetails) {
const tokenError: string = imperativeError.mDetails.additionalDetails;
const isTokenAuth = await AuthUtils.isUsingTokenAuth(profile.name);

if (tokenError.includes("Token is not valid or expired.") || isTokenAuth) {
const message = vscode.l10n.t("Log in to Authentication Service");
const userResp = await Gui.showMessage(correlation?.message ?? imperativeError.message, {
items: [message],
vsCodeOpts: { modal: true },
});
return userResp === message ? Constants.PROFILES_CACHE.ssoLogin(null, profile.name) : false;
}
}
const checkCredsButton = vscode.l10n.t("Update Credentials");
const creds = await Gui.errorMessage(correlation?.message ?? imperativeError.message, {
items: [checkCredsButton],
vsCodeOpts: { modal: true },
}).then(async (selection) => {
if (selection !== checkCredsButton) {
return;
}
return Constants.PROFILES_CACHE.promptCredentials(profile, true);
});
return creds != null ? true : false;
}

public static promptForAuthError(err: Error, profile: imperative.IProfileLoaded): void {
public static async promptForAuthOnError(err: Error, profile: imperative.IProfileLoaded): Promise<void> {
if (
err instanceof imperative.ImperativeError &&
profile != null &&
(Number(err.errorCode) === imperative.RestConstants.HTTP_STATUS_401 ||
err.message.includes("All configured authentication methods failed"))
) {
const correlation = ErrorCorrelator.getInstance().correlateError(ZoweExplorerApiType.All, err, {
const errorCorrelation = ErrorCorrelator.getInstance().correlateError(ZoweExplorerApiType.All, err, {
templateArgs: {
profileName: profile.name
}
profileName: profile.name,
},
});
void AuthUtils.promptForAuthentication(err, profile, correlation).catch(
(error) => error instanceof Error && ZoweLogger.error(error.message)
);
await AuthHandler.promptForAuthentication(err, profile, {
ssoLogin: Constants.PROFILES_CACHE.ssoLogin.bind(Constants.PROFILES_CACHE),
promptCredentials: Constants.PROFILES_CACHE.promptCredentials.bind(Constants.PROFILES_CACHE),
isUsingTokenAuth: await AuthUtils.isUsingTokenAuth(profile.name),
errorCorrelation,
}).catch((error) => error instanceof Error && ZoweLogger.error(error.message));
}
}

Expand Down Expand Up @@ -97,7 +69,7 @@ export class AuthUtils {
ZoweLogger.error(`${errorDetails.toString()}\n` + util.inspect({ errorDetails, ...{ ...moreInfo, profile: undefined } }, { depth: null }));

const profile = typeof moreInfo.profile === "string" ? Constants.PROFILES_CACHE.loadNamedProfile(moreInfo.profile) : moreInfo?.profile;
const correlation = ErrorCorrelator.getInstance().correlateError(moreInfo?.apiType ?? ZoweExplorerApiType.All, errorDetails, {
const errorCorrelation = ErrorCorrelator.getInstance().correlateError(moreInfo?.apiType ?? ZoweExplorerApiType.All, errorDetails, {
profileType: profile?.type,
...Object.keys(moreInfo).reduce((all, k) => (typeof moreInfo[k] === "string" ? { ...all, [k]: moreInfo[k] } : all), {}),
templateArgs: { profileName: profile?.name ?? "", ...moreInfo?.templateArgs },
Expand All @@ -114,14 +86,19 @@ export class AuthUtils {
(httpErrorCode === imperative.RestConstants.HTTP_STATUS_401 ||
imperativeError.message.includes("All configured authentication methods failed"))
) {
return AuthUtils.promptForAuthentication(imperativeError, profile, correlation);
return AuthHandler.promptForAuthentication(imperativeError, profile, {
ssoLogin: Constants.PROFILES_CACHE.ssoLogin.bind(Constants.PROFILES_CACHE),
promptCredentials: Constants.PROFILES_CACHE.promptCredentials.bind(Constants.PROFILES_CACHE),
isUsingTokenAuth: await AuthUtils.isUsingTokenAuth(profile.name),
errorCorrelation,
});
}
}
if (errorDetails.toString().includes("Could not find profile")) {
return false;
}

void ErrorCorrelator.getInstance().displayCorrelatedError(correlation, { templateArgs: { profileName: profile?.name ?? "" } });
void ErrorCorrelator.getInstance().displayCorrelatedError(errorCorrelation, { templateArgs: { profileName: profile?.name ?? "" } });
return false;
}

Expand Down Expand Up @@ -195,13 +172,9 @@ export class AuthUtils {
* @returns {Promise<boolean>} a boolean representing whether token based auth is being used or not
*/
public static async isUsingTokenAuth(profileName: string): Promise<boolean> {
const secureProfileProps = await Constants.PROFILES_CACHE.getSecurePropsForProfile(profileName);
const profileUsesBasicAuth = secureProfileProps.includes("user") && secureProfileProps.includes("password");
if (secureProfileProps.includes("tokenValue")) {
return secureProfileProps.includes("tokenValue") && !profileUsesBasicAuth;
}
const secureProps = await Constants.PROFILES_CACHE.getSecurePropsForProfile(profileName);
const baseProfile = Constants.PROFILES_CACHE.getDefaultProfile("base");
const secureBaseProfileProps = await Constants.PROFILES_CACHE.getSecurePropsForProfile(baseProfile?.name);
return secureBaseProfileProps.includes("tokenValue") && !profileUsesBasicAuth;
const baseSecureProps = await Constants.PROFILES_CACHE.getSecurePropsForProfile(baseProfile?.name);
return AuthHandler.isUsingTokenAuth(secureProps, baseSecureProps);
}
}

0 comments on commit aa5251c

Please sign in to comment.