Skip to content

Commit 6c18caf

Browse files
Merge pull request #238 from gjsjohnmurray/fix-233
If connect with stored password fails, delete and re-prompt
2 parents eb0e39d + 9c5c65a commit 6c18caf

File tree

5 files changed

+84
-51
lines changed

5 files changed

+84
-51
lines changed

src/api/getServerSpec.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,17 @@
11
import * as vscode from "vscode";
22
import { IServerSpec } from "@intersystems-community/intersystems-servermanager";
33

4-
interface ICredentialSet {
5-
username: string;
6-
password: string;
7-
}
8-
9-
export let credentialCache = new Map<string, ICredentialSet>();
10-
114
/**
125
* Get a server specification.
136
*
147
* @param name The name.
158
* @param scope The settings scope to use for the lookup.
16-
* @param flushCredentialCache Flush the session's cache of credentials obtained from keystore and/or user prompting.
17-
* @param noCredentials Set username and password as undefined; do not fetch credentials from anywhere.
189
* @returns Server specification or undefined.
1910
*/
2011
export async function getServerSpec(
2112
name: string,
2213
scope?: vscode.ConfigurationScope,
23-
flushCredentialCache: boolean = false,
24-
noCredentials: boolean = false,
2514
): Promise<IServerSpec | undefined> {
26-
if (flushCredentialCache) {
27-
credentialCache[name] = undefined;
28-
}
2915
// To avoid breaking existing users, continue to return a default server definition even after we dropped that feature
3016
let server: IServerSpec | undefined = vscode.workspace.getConfiguration("intersystems.servers", scope).get(name) || legacyEmbeddedServer(name);
3117

src/authenticationProvider.ts

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
} from "vscode";
1515
import { ServerManagerAuthenticationSession } from "./authenticationSession";
1616
import { globalState } from "./extension";
17+
import { getServerSpec } from "./api/getServerSpec";
18+
import { makeRESTRequest } from "./makeRESTRequest";
1719

1820
export const AUTHENTICATION_PROVIDER = "intersystems-server-credentials";
1921
const AUTHENTICATION_PROVIDER_LABEL = "InterSystems Server Credentials";
@@ -35,6 +37,7 @@ export class ServerManagerAuthenticationProvider implements AuthenticationProvid
3537
private readonly _secretStorage;
3638

3739
private _sessions: ServerManagerAuthenticationSession[] = [];
40+
private _checkedSessions: ServerManagerAuthenticationSession[] = [];
3841

3942
private _serverManagerExtension = extensions.getExtension("intersystems-community.servermanager");
4043

@@ -52,7 +55,7 @@ export class ServerManagerAuthenticationProvider implements AuthenticationProvid
5255
this._initializedDisposable?.dispose();
5356
}
5457

55-
// This function is called first when `vscode.authentication.getSessions` is called.
58+
// This function is called first when `vscode.authentication.getSession` is called.
5659
public async getSessions(scopes: string[] = []): Promise<readonly AuthenticationSession[]> {
5760
await this._ensureInitialized();
5861
let sessions = this._sessions;
@@ -61,7 +64,13 @@ export class ServerManagerAuthenticationProvider implements AuthenticationProvid
6164
for (let index = 0; index < scopes.length; index++) {
6265
sessions = sessions.filter((session) => session.scopes[index] === scopes[index].toLowerCase());
6366
}
64-
return sessions;
67+
68+
if (sessions.length === 1) {
69+
if (!(await this._isStillValid(sessions[0]))) {
70+
sessions = [];
71+
}
72+
}
73+
return sessions || [];
6574
}
6675

6776
// This function is called after `this.getSessions` is called, and only when:
@@ -104,9 +113,17 @@ export class ServerManagerAuthenticationProvider implements AuthenticationProvid
104113

105114
// Return existing session if found
106115
const sessionId = ServerManagerAuthenticationProvider.sessionId(serverName, userName);
107-
const existingSession = this._sessions.find((s) => s.id === sessionId);
116+
let existingSession = this._sessions.find((s) => s.id === sessionId);
108117
if (existingSession) {
109-
return existingSession;
118+
if (this._checkedSessions.find((s) => s.id === sessionId)) {
119+
return existingSession;
120+
}
121+
122+
// Check if the session is still valid
123+
if (await this._isStillValid(existingSession)) {
124+
this._checkedSessions.push(existingSession);
125+
return existingSession;
126+
}
110127
}
111128

112129
let password: string | undefined = "";
@@ -190,25 +207,52 @@ export class ServerManagerAuthenticationProvider implements AuthenticationProvid
190207
return session;
191208
}
192209

210+
private async _isStillValid(session: ServerManagerAuthenticationSession): Promise<boolean> {
211+
if (this._checkedSessions.find((s) => s.id === session.id)) {
212+
return true;
213+
}
214+
const serverSpec = await getServerSpec(session.serverName);
215+
if (serverSpec) {
216+
serverSpec.username = session.userName;
217+
serverSpec.password = session.accessToken;
218+
const response = await makeRESTRequest("HEAD", serverSpec);
219+
if (response?.status === 401) {
220+
await this._removeSession(session.id, true);
221+
return false;
222+
}
223+
}
224+
this._checkedSessions.push(session);
225+
return true;
226+
}
227+
193228
// This function is called when the end user signs out of the account.
194229
public async removeSession(sessionId: string): Promise<void> {
230+
this._removeSession(sessionId);
231+
}
232+
233+
private async _removeSession(sessionId: string, alwaysDeletePassword = false): Promise<void> {
195234
const index = this._sessions.findIndex((item) => item.id === sessionId);
196235
const session = this._sessions[index];
197236

198-
let deletePassword = false;
199237
const credentialKey = ServerManagerAuthenticationProvider.credentialKey(sessionId);
200-
if (await this.secretStorage.get(credentialKey)) {
201-
const passwordOption = workspace.getConfiguration("intersystemsServerManager.credentialsProvider")
202-
.get<string>("deletePasswordOnSignout", "ask");
203-
deletePassword = (passwordOption === "always");
204-
if (passwordOption === "ask") {
205-
const choice = await window.showWarningMessage(
206-
`Do you want to keep the password or delete it?`,
207-
{ detail: `The ${AUTHENTICATION_PROVIDER_LABEL} account you signed out (${session.account.label}) is currently storing its password securely on your workstation.`, modal: true },
208-
{ title: "Keep", isCloseAffordance: true },
209-
{ title: "Delete", isCloseAffordance: false },
210-
);
211-
deletePassword = (choice?.title === "Delete");
238+
let deletePassword = false;
239+
const hasStoredPassword = await this.secretStorage.get(credentialKey) !== undefined;
240+
if (alwaysDeletePassword) {
241+
deletePassword = hasStoredPassword;
242+
} else {
243+
if (hasStoredPassword) {
244+
const passwordOption = workspace.getConfiguration("intersystemsServerManager.credentialsProvider")
245+
.get<string>("deletePasswordOnSignout", "ask");
246+
deletePassword = (passwordOption === "always");
247+
if (passwordOption === "ask") {
248+
const choice = await window.showWarningMessage(
249+
`Do you want to keep the password or delete it?`,
250+
{ detail: `The ${AUTHENTICATION_PROVIDER_LABEL} account you signed out (${session.account.label}) is currently storing its password securely on your workstation.`, modal: true },
251+
{ title: "Keep", isCloseAffordance: true },
252+
{ title: "Delete", isCloseAffordance: false },
253+
);
254+
deletePassword = (choice?.title === "Delete");
255+
}
212256
}
213257
}
214258
if (deletePassword) {

src/extension.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ export function activate(context: vscode.ExtensionContext) {
254254
if (pathParts && pathParts.length === 4) {
255255
const serverName = pathParts[1];
256256
const namespace = pathParts[3];
257-
const serverSpec = await getServerSpec(serverName, undefined, undefined, true);
257+
const serverSpec = await getServerSpec(serverName);
258258
if (serverSpec) {
259259
const ISFS_ID = "intersystems-community.vscode-objectscript";
260260
const isfsExtension = vscode.extensions.getExtension(ISFS_ID);
@@ -379,28 +379,28 @@ export function activate(context: vscode.ExtensionContext) {
379379
/**
380380
* Get specification for the named server.
381381
*
382-
* If the `"intersystemsServerManager.authentication.provider"` setting is "intersystems-server-credentials":
383-
* - the returned object will not contain `password`. To get this:
382+
* The returned object will not contain `password`. To get that:
384383
* ```
385-
* const session = await vscode.authentication.getSession('intersystems-server-credentials', [serverSpec.name, serverSpec.username]);
384+
* const session: vscode.AuthenticationSession = await vscode.authentication.getSession('intersystems-server-credentials', [serverSpec.name, serverSpec.username]);
386385
* ```
387386
* The `accessToken` property of the returned [`AuthenticationSession`](https://code.visualstudio.com/api/references/vscode-api#AuthenticationSession) is the password.
388-
* - `flushCredentialsCache` param will be ignored;
389-
* - `noCredentials` property of `options` param has no effect;
387+
*
388+
* The `flushCredentialsCache` param is obsolete and has no effect;
389+
* The `noCredentials` property of `options` param is obsolete and has no effect;
390390
*
391391
* @param name Name of the server, used as the key into the 'intersystems.servers' settings object
392392
* @param scope Settings scope to look in.
393-
* @param flushCredentialCache If passed as true, flush extension's credential cache.
393+
* @param flushCredentialCache Obsolete, has no effect.
394394
* @param options
395395
* @returns { IServerSpec } Server specification object.
396396
*/
397397
async getServerSpec(
398398
name: string,
399399
scope?: vscode.ConfigurationScope,
400400
flushCredentialCache: boolean = false,
401-
options?: { hideFromRecents?: boolean, noCredentials?: boolean },
401+
options?: { hideFromRecents?: boolean, /* Obsolete */ noCredentials?: boolean },
402402
): Promise<IServerSpec | undefined> {
403-
const spec = await getServerSpec(name, scope, flushCredentialCache, options?.noCredentials);
403+
const spec = await getServerSpec(name, scope);
404404
if (spec && !options?.hideFromRecents) {
405405
await view.addToRecents(name);
406406
}

src/makeRESTRequest.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ export async function makeRESTRequest(
165165
return respdata;
166166
} catch (error) {
167167
console.log(error);
168-
return undefined;
168+
return error.response;
169169
}
170170
}
171171

@@ -176,7 +176,7 @@ export async function makeRESTRequest(
176176
*/
177177
export async function logout(serverName: string) {
178178

179-
const server = await getServerSpec(serverName, undefined, false, true);
179+
const server = await getServerSpec(serverName, undefined);
180180

181181
if (!server) {
182182
return;

src/ui/serverManagerView.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as vscode from "vscode";
22
import { getServerNames } from "../api/getServerNames";
3-
import { credentialCache, getServerSpec } from "../api/getServerSpec";
3+
import { getServerSpec } from "../api/getServerSpec";
44
import { getServerSummary } from "../api/getServerSummary";
55
import { IServerName } from "@intersystems-community/intersystems-servermanager";
66
import { makeRESTRequest } from "../makeRESTRequest";
@@ -383,10 +383,14 @@ async function serverFeatures(element: ServerTreeItem, params?: any): Promise<Fe
383383
return undefined;
384384
}
385385

386-
const response = await makeRESTRequest("HEAD", serverSpec);
387-
if (!response || response.status !== 200) {
386+
let response = await makeRESTRequest("HEAD", serverSpec);
387+
if (response?.status === 401) {
388+
// Authentication error, so retry in case first attempt cleared a no-longer-valid stored password
389+
serverSpec.password = undefined;
390+
response = await makeRESTRequest("HEAD", serverSpec);
391+
}
392+
if (response?.status !== 200) {
388393
children.push(new OfflineTreeItem({ parent: element, label: name, id: name }, serverSpec.username || 'UnknownUser'));
389-
credentialCache[name] = undefined;
390394
} else {
391395
children.push(new NamespacesTreeItem({ parent: element, label: name, id: name }, element.name, serverSpec.username || 'UnknownUser'));
392396
}
@@ -459,12 +463,11 @@ async function serverNamespaces(element: ServerTreeItem, params?: any): Promise<
459463
}
460464

461465
const response = await makeRESTRequest("GET", serverSpec);
462-
if (!response || response.status !== 200) {
466+
if (response?.status !== 200) {
463467
children.push(new OfflineTreeItem({ parent: element, label: name, id: name }, serverSpec.username || 'UnknownUser'));
464-
credentialCache[params.serverName] = undefined;
465468
} else {
466469
const serverApiVersion = response.data.result.content.api;
467-
response.data.result.content.namespaces.map((namespace) => {
470+
response.data.result.content.namespaces.map((namespace: string) => {
468471
children.push(new NamespaceTreeItem({ parent: element, label: name, id: name }, namespace, name, serverApiVersion));
469472
});
470473
}
@@ -557,7 +560,7 @@ async function namespaceProjects(element: ProjectsTreeItem, params?: any): Promi
557560
{ apiVersion: 1, namespace: params.ns, path: "/action/query" },
558561
{ query: "SELECT Name, Description FROM %Studio.Project", parameters: [] }
559562
);
560-
if (response !== undefined) {
563+
if (response?.status === 200) {
561564
if (response.data.result.content === undefined) {
562565
let message;
563566
if (response.data.status?.errors[0]?.code === 5540) {
@@ -643,7 +646,7 @@ async function namespaceWebApps(element: ProjectsTreeItem, params?: any): Promis
643646
serverSpec,
644647
{ apiVersion: 1, namespace: "%SYS", path: `/cspapps/${params.ns}` }
645648
);
646-
if (response !== undefined) {
649+
if (response?.status === 200) {
647650
if (response.data.result.content === undefined) {
648651
vscode.window.showErrorMessage(response.data.status.summary);
649652
return undefined;

0 commit comments

Comments
 (0)