diff --git a/package.json b/package.json index 5dc8857..3cbb328 100644 --- a/package.json +++ b/package.json @@ -375,6 +375,11 @@ "command": "intersystems-community.servermanager.viewWebApp", "title": "View Files in Web Application", "icon": "$(eye)" + }, + { + "command": "intersystems-community.servermanager.signOut", + "category": "InterSystems Server Manager", + "title": "Sign Out of Accounts..." } ], "submenus": [ @@ -541,6 +546,11 @@ "when": "view == intersystems-community_servermanager", "group": "1_edit" }, + { + "command": "intersystems-community.servermanager.signOut", + "when": "view == intersystems-community_servermanager", + "group": "1_edit" + }, { "command": "intersystems-community.servermanager.importServers", "when": "view == intersystems-community_servermanager && isWindows", diff --git a/src/authenticationProvider.ts b/src/authenticationProvider.ts index d7fdfce..1ca8740 100644 --- a/src/authenticationProvider.ts +++ b/src/authenticationProvider.ts @@ -165,7 +165,6 @@ export class ServerManagerAuthenticationProvider implements AuthenticationProvid const enteredPassword = inputBox.value; if (secretStorage && enteredPassword) { await secretStorage.store(credentialKey, enteredPassword); - console.log(`Stored password at ${credentialKey}`); } // Resolve the promise and tidy up resolve(enteredPassword); @@ -270,7 +269,6 @@ export class ServerManagerAuthenticationProvider implements AuthenticationProvid if (deletePassword) { // Delete from secret storage await this.secretStorage.delete(credentialKey); - console.log(`${AUTHENTICATION_PROVIDER_LABEL}: Deleted password at ${credentialKey}`); } if (index > -1) { // Remove session here so we don't store it @@ -280,6 +278,45 @@ export class ServerManagerAuthenticationProvider implements AuthenticationProvid this._onDidChangeSessions.fire({ added: [], removed: [session], changed: [] }); } + public async removeSessions(sessionIds: string[]): Promise { + const storedPasswordCredKeys: string[] = []; + const removed: AuthenticationSession[] = []; + await Promise.allSettled(sessionIds.map(async (sessionId) => { + const index = this._sessions.findIndex((item) => item.id === sessionId); + const session = this._sessions[index]; + const credentialKey = ServerManagerAuthenticationProvider.credentialKey(sessionId); + if (await this.secretStorage.get(credentialKey) !== undefined) { + storedPasswordCredKeys.push(credentialKey); + } + if (index > -1) { + this._sessions.splice(index, 1); + } + removed.push(session); + })); + if (storedPasswordCredKeys.length) { + const passwordOption = workspace.getConfiguration("intersystemsServerManager.credentialsProvider") + .get("deletePasswordOnSignout", "ask"); + let deletePasswords = (passwordOption === "always"); + if (passwordOption === "ask") { + const choice = await window.showWarningMessage( + `Do you want to keep the stored passwords or delete them?`, + { + detail: `${storedPasswordCredKeys.length == sessionIds.length ? "All" : "Some" + } of the ${AUTHENTICATION_PROVIDER_LABEL} accounts you signed out are currently storing their passwords securely on your workstation.`, modal: true + }, + { title: "Keep", isCloseAffordance: true }, + { title: "Delete", isCloseAffordance: false }, + ); + deletePasswords = (choice?.title === "Delete"); + } + if (deletePasswords) { + await Promise.allSettled(storedPasswordCredKeys.map((e) => this.secretStorage.delete(e))); + } + } + await this._storeStrippedSessions(); + this._onDidChangeSessions.fire({ added: [], removed, changed: [] }); + } + private async _ensureInitialized(): Promise { if (this._initializedDisposable === undefined) { diff --git a/src/commonActivate.ts b/src/commonActivate.ts index 4e38b2c..bc9ac28 100644 --- a/src/commonActivate.ts +++ b/src/commonActivate.ts @@ -76,13 +76,14 @@ export function commonActivate(context: vscode.ExtensionContext, view: ServerMan } }; + const authProvider = new ServerManagerAuthenticationProvider(context.secrets); context.subscriptions.push( // Register our authentication provider. NOTE: this will register the provider globally which means that // any other extension can request to use use this provider via the `vscode.authentication.getSession` API. vscode.authentication.registerAuthenticationProvider( ServerManagerAuthenticationProvider.id, ServerManagerAuthenticationProvider.label, - new ServerManagerAuthenticationProvider(context.secrets), + authProvider, { supportsMultipleAccounts: true }, ), // Ensure cookies do not survive an account sign-out @@ -136,7 +137,7 @@ export function commonActivate(context: vscode.ExtensionContext, view: ServerMan } const choice = await vscode.window.showQuickPick(options, { ignoreFocusOut: true, - title: "Pick a settngs scope in which to add the server definition" + title: "Pick a settings scope in which to add the server definition" }); if (!choice) return; scope = choice.scope; @@ -330,6 +331,16 @@ export function commonActivate(context: vscode.ExtensionContext, view: ServerMan await addWorkspaceFolderAsync(true, true, webAppTreeItem?.parent?.parent, undefined, webAppTreeItem?.name); }), vscode.workspace.onDidChangeWorkspaceFolders(() => view.refreshTree()), + vscode.commands.registerCommand(`${extensionId}.signOut`, async () => { + const sessions = await authProvider.getSessions(undefined, {}).catch(() => { }); + if (!sessions?.length) { + vscode.window.showInformationMessage("There are no stored accounts to sign out of.", "Dismiss"); + return; + } + const picks = await vscode.window.showQuickPick(sessions.map((s) => s.account), { canPickMany: true, title: "Pick the accounts to sign out of" }); + if (!picks?.length) return; + return authProvider.removeSessions(picks.map((p) => p.id)); + }), // Listen for relevant configuration changes vscode.workspace.onDidChangeConfiguration((e) => { if (e.affectsConfiguration("intersystems.servers") || e.affectsConfiguration("objectscript.conn")) {