Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Debounce credential prompts and check profile locks in more places #3480

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f693249
refactor: check locks in dataset FS remote lookup; update AuthHandler…
traeok Feb 25, 2025
2bc8c44
refactor: handle auth prompts for more USS scenarios
traeok Feb 25, 2025
9b30aeb
refactor: use latest profile in more places in FSPs; part 2 to recent…
traeok Feb 25, 2025
f8aeab3
fix: debounce auth prompts during parallel requests
traeok Feb 25, 2025
4885d85
feat: useModal optional property for auth prompts
traeok Feb 25, 2025
0e2e471
fix(AuthHandler): use modal prompt for FSPs
traeok Feb 25, 2025
9c43211
refactor: debounce prompts using separate prompt mutexes
traeok Feb 25, 2025
d97d5c1
fix(tests): use fake timers in waitForUnlock test
traeok Feb 25, 2025
3eeb67b
refactor: unlock all profiles after vault changed
traeok Feb 25, 2025
3082d45
fix tests; update changelog
traeok Feb 25, 2025
bca7f2a
fix: await AuthHandler.shouldHandleAuthError
traeok Feb 25, 2025
9da0610
chore: update changelog
traeok Feb 25, 2025
859a088
wip: AuthHandler & AuthUtils patch coverage
traeok Feb 26, 2025
054ef4b
fix(AuthHandler): remove unnecessary branch
traeok Feb 26, 2025
b3767e3
tests: Data Set and USS profile lock tests
traeok Feb 27, 2025
5ccac6c
refactor: use ZoweLogger.warn instead of debug
traeok Feb 27, 2025
980b31c
Merge branch 'main' into fix/cred-loop-virtual-workspace
traeok Feb 27, 2025
66fe610
refactor: remove unused useModal param
traeok Feb 27, 2025
c4f4c00
chore: add ZE API changelog
traeok Feb 27, 2025
adcc1cc
tests: fix logic & remove skip on listFiles cases
traeok Feb 27, 2025
3c6b2b4
more test cases for profile lock checks
traeok Feb 27, 2025
3385bb0
Merge branch 'main' into fix/cred-loop-virtual-workspace
traeok Feb 27, 2025
81d2501
chore: fix changelog
traeok Feb 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 56 additions & 4 deletions packages/zowe-explorer-api/src/profiles/AuthHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,14 @@ export type AuthPromptParams = {
authMethods: IAuthMethods;
// Error encountered from API call
imperativeError: imperative.ImperativeError;
// Whether to show the dialog as a modal
useModal?: boolean;
};

export type ProfileLike = string | imperative.IProfileLoaded;
export class AuthHandler {
private static profileLocks: Map<string, Mutex> = new Map();
private static authPromptLocks = new Map<string, Mutex>();
private static profileLocks = new Map<string, Mutex>();
private static enabledProfileTypes: Set<string> = new Set(["zosmf"]);

/**
Expand Down Expand Up @@ -78,6 +81,7 @@ export class AuthHandler {
*/
public static unlockProfile(profile: ProfileLike, refreshResources?: boolean): void {
const profileName = AuthHandler.getProfileName(profile);
this.authPromptLocks.get(profileName)?.release();
const mutex = this.profileLocks.get(profileName);
// If a mutex doesn't exist for this profile or the mutex is no longer locked, return
if (mutex == null || !mutex.isLocked()) {
Expand All @@ -99,6 +103,26 @@ export class AuthHandler {
}
}

/**
* Determines whether to handle an authentication error for a given profile.
* This uses a mutex to prevent additional authentication prompts until the first prompt is resolved.
* @param profileName The name of the profile to check
* @returns {boolean} Whether to handle the authentication error
*/
public static async shouldHandleAuthError(profileName: string): Promise<boolean> {
if (!this.authPromptLocks.has(profileName)) {
this.authPromptLocks.set(profileName, new Mutex());
}

const mutex = this.authPromptLocks.get(profileName);
if (mutex.isLocked()) {
return false;
}

await mutex.acquire();
return true;
}

/**
* Prompts the user to authenticate over SSO or a credential prompt in the event of an error.
* @param profile The profile to authenticate
Expand All @@ -114,7 +138,7 @@ export class AuthHandler {
const message = "Log in to Authentication Service";
const userResp = await Gui.showMessage(params.errorCorrelation?.message ?? params.imperativeError.message, {
items: [message],
vsCodeOpts: { modal: true },
vsCodeOpts: { modal: params.useModal },
});
if (userResp === message && (await params.authMethods.ssoLogin(null, profileName))) {
// Unlock profile so it can be used again
Expand All @@ -129,7 +153,7 @@ export class AuthHandler {
const checkCredsButton = "Update Credentials";
const creds = await Gui.errorMessage(params.errorCorrelation?.message ?? params.imperativeError.message, {
items: [checkCredsButton],
vsCodeOpts: { modal: true },
vsCodeOpts: { modal: params.useModal },
}).then(async (selection) => {
if (selection !== checkCredsButton) {
return;
Expand Down Expand Up @@ -194,7 +218,35 @@ export class AuthHandler {
return;
}

return this.profileLocks.get(profileName)?.waitForUnlock();
const mutex = this.profileLocks.get(profileName);
// If the mutex isn't locked, no need to wait
if (!mutex.isLocked()) {
return;
}

// Wait for the mutex to be unlocked with a timeout to prevent indefinite waiting
const timeoutMs = 30000; // 30 seconds timeout
const timeoutPromise = new Promise<void>((_, reject) => {
setTimeout(() => {
reject(new Error(`Timeout waiting for profile ${profileName} to be unlocked`));
}, timeoutMs);
});

try {
await Promise.race([mutex.waitForUnlock(), timeoutPromise]);

// Add a small delay after unlock to ensure credentials are fully updated
// before allowing multiple requests to proceed
await new Promise((resolve) => setTimeout(resolve, 500));
} catch (error) {
// If we hit the timeout, log it but don't throw to allow operation to continue
if (error instanceof Error && error.message.includes("Timeout waiting for profile")) {
// Log the timeout but continue - we'll use a no-op since we don't have access to the logger in the API
// This is acceptable since this is just a fallback for an edge case where the user did not respond to a credential prompt in time
} else {
throw error;
}
}
}

/**
Expand Down
77 changes: 68 additions & 9 deletions packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,26 +89,38 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem
}

ZoweLogger.trace(`[DatasetFSProvider] stat is locating resource ${uri.toString()}`);
const profile = Profiles.getInstance().loadNamedProfile(uriInfo.profile.name);

// Locate the resource using the profile in the given URI.
let resp;
const isPdsMember = !FsDatasetsUtils.isPdsEntry(entry) && (entry as DsEntry).isMember;
try {
// Wait for any ongoing authentication process to complete
await AuthHandler.waitForUnlock(uriInfo.profile);

// Check if the profile is locked (indicating an auth error is being handled)
// If it's locked, we should wait and not make additional requests
if (AuthHandler.isProfileLocked(uriInfo.profile)) {
ZoweLogger.debug(`[DatasetFSProvider] Profile ${uriInfo.profile.name} is locked, waiting for authentication`);
return entry;
}

if (isPdsMember) {
// PDS member
const pds = this._lookupParentDirectory(uri);
resp = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).allMembers(pds.name, { attributes: true });
resp = await ZoweExplorerApiRegister.getMvsApi(profile).allMembers(pds.name, { attributes: true });
} else {
// Data Set
const dsPath = (entry.metadata as DsEntryMetadata).extensionRemovedFromPath();
resp = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).dataSet(path.posix.basename(dsPath), {
resp = await ZoweExplorerApiRegister.getMvsApi(profile).dataSet(path.posix.basename(dsPath), {
attributes: true,
});
}
} catch (err) {
if (err instanceof Error) {
ZoweLogger.error(err.message);
}
await AuthUtils.handleProfileAuthOnError(err, profile);
throw err;
}

Expand All @@ -133,7 +145,18 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem
private async fetchEntriesForProfile(uri: vscode.Uri, uriInfo: UriFsInfo, pattern: string): Promise<FilterEntry> {
const profileEntry = this._lookupAsDirectory(uri, false) as FilterEntry;

const mvsApi = ZoweExplorerApiRegister.getMvsApi(uriInfo.profile);
// Wait for any ongoing authentication process to complete
await AuthHandler.waitForUnlock(uriInfo.profile);

// Check if the profile is locked (indicating an auth error is being handled)
// If it's locked, we should wait and not make additional requests
if (AuthHandler.isProfileLocked(uriInfo.profile)) {
ZoweLogger.debug(`[DatasetFSProvider] Profile ${uriInfo.profile.name} is locked, waiting for authentication`);
return profileEntry;
}

const profile = Profiles.getInstance().loadNamedProfile(uriInfo.profile.name);
const mvsApi = ZoweExplorerApiRegister.getMvsApi(profile);
const datasetResponses: IZosFilesResponse[] = [];
const dsPatterns = [
...new Set(
Expand All @@ -153,6 +176,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem
}
}
} catch (err) {
await AuthUtils.handleProfileAuthOnError(err, uriInfo.profile);
this._handleError(err, {
additionalContext: vscode.l10n.t("Failed to list datasets"),
retry: {
Expand Down Expand Up @@ -196,8 +220,18 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem
private async fetchEntriesForDataset(entry: PdsEntry, uri: vscode.Uri, uriInfo: UriFsInfo): Promise<void> {
let members: IZosFilesResponse;
try {
// Wait for any ongoing authentication process to complete
await AuthHandler.waitForUnlock(entry.metadata.profile);
members = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).allMembers(path.posix.basename(uri.path));

// Check if the profile is locked (indicating an auth error is being handled)
// If it's locked, we should wait and not make additional requests
if (AuthHandler.isProfileLocked(entry.metadata.profile)) {
ZoweLogger.debug(`[DatasetFSProvider] Profile ${entry.metadata.profile.name} is locked, waiting for authentication`);
return;
}

const profile = Profiles.getInstance().loadNamedProfile(entry.metadata.profile.name);
members = await ZoweExplorerApiRegister.getMvsApi(profile).allMembers(path.posix.basename(uri.path));
} catch (err) {
await AuthUtils.handleProfileAuthOnError(err, uriInfo.profile);
throw err;
Expand Down Expand Up @@ -230,10 +264,25 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem
// /DATA.SET/MEMBER
const uriPath = uri.path.substring(uriInfo.slashAfterProfilePos + 1).split("/");
const pdsMember = uriPath.length === 2;

// Wait for any ongoing authentication process to complete
await AuthHandler.waitForUnlock(uriInfo.profile);

// Check if the profile is locked (indicating an auth error is being handled)
// If it's locked, we should wait and not make additional requests
if (AuthHandler.isProfileLocked(uriInfo.profile)) {
ZoweLogger.debug(`[DatasetFSProvider] Profile ${uriInfo.profile.name} is locked, waiting for authentication`);
if (entryExists) {
return entry;
}
throw vscode.FileSystemError.FileNotFound(uri);
}

if (!entryExists) {
try {
const profile = Profiles.getInstance().loadNamedProfile(uriInfo.profile.name);
if (pdsMember) {
const resp = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).allMembers(uriPath[0]);
const resp = await ZoweExplorerApiRegister.getMvsApi(profile).allMembers(uriPath[0]);
entryIsDir = false;
const memberName = path.parse(uriPath[1]).name;
if (
Expand All @@ -244,7 +293,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem
throw vscode.FileSystemError.FileNotFound(uri);
}
} else {
const resp = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).dataSet(uriPath[0], {
const resp = await ZoweExplorerApiRegister.getMvsApi(profile).dataSet(uriPath[0], {
attributes: true,
});
if (resp.success && resp.apiResponse?.items?.length > 0) {
Expand All @@ -257,6 +306,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem
if (err instanceof Error) {
ZoweLogger.error(err.message);
}
await AuthUtils.handleProfileAuthOnError(err, uriInfo.profile);
throw err;
}
}
Expand Down Expand Up @@ -382,14 +432,23 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem
let dsEntry = this._lookupAsFile(uri, { silent: true }) as DsEntry | undefined;
const bufBuilder = new BufferBuilder();
const metadata = dsEntry?.metadata ?? this._getInfoFromUri(uri);
const profile = Profiles.getInstance().loadNamedProfile(dsEntry?.metadata.profile.name);
const profile = Profiles.getInstance().loadNamedProfile(metadata.profile.name);
const profileEncoding = dsEntry?.encoding ? null : profile.profile?.encoding; // use profile encoding rather than metadata encoding
try {
// Wait for any ongoing authentication process to complete
await AuthHandler.waitForUnlock(metadata.profile);
const resp = await ZoweExplorerApiRegister.getMvsApi(metadata.profile).getContents(metadata.dsName, {

// Check if the profile is locked (indicating an auth error is being handled)
// If it's locked, we should wait and not make additional requests
if (AuthHandler.isProfileLocked(metadata.profile)) {
ZoweLogger.debug(`[DatasetFSProvider] Profile ${metadata.profile.name} is locked, waiting for authentication`);
return null;
}

const resp = await ZoweExplorerApiRegister.getMvsApi(profile).getContents(metadata.dsName, {
binary: dsEntry?.encoding?.kind === "binary",
encoding: dsEntry?.encoding?.kind === "other" ? dsEntry?.encoding.codepage : profileEncoding,
responseTimeout: metadata.profile.profile?.responseTimeout,
responseTimeout: profile.profile?.responseTimeout,
returnEtag: true,
stream: bufBuilder,
});
Expand Down
Loading
Loading