Skip to content

Commit ca141eb

Browse files
Merge pull request #146 from gjsjohnmurray/pre-do-129
Migrate Server Manager 2 stored passwords
2 parents 439862c + 902352e commit ca141eb

File tree

4 files changed

+124
-8
lines changed

4 files changed

+124
-8
lines changed

package.json

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
"onCommand:intersystems-community.servermanager.addServer",
7272
"onCommand:intersystems-community.servermanager.storePassword",
7373
"onCommand:intersystems-community.servermanager.clearPassword",
74+
"onCommand:intersystems-community.servermanager.migratePasswords",
7475
"onCommand:intersystems-community.servermanager.importServers",
7576
"onCommand:intersystems-community.servermanager-credentials.testLogin",
7677
"onCommand:intersystems-community.servermanager-credentials.testScopedLogin",
@@ -323,6 +324,11 @@
323324
"category": "InterSystems Server Manager",
324325
"title": "Clear Password from Keychain"
325326
},
327+
{
328+
"command": "intersystems-community.servermanager.migratePasswords",
329+
"category": "InterSystems Server Manager",
330+
"title": "Migrate Legacy Passwords"
331+
},
326332
{
327333
"command": "intersystems-community.servermanager.importServers",
328334
"category": "InterSystems Server Manager",
@@ -479,6 +485,18 @@
479485
"command": "intersystems-community.servermanager.importServers",
480486
"when": "isWindows"
481487
},
488+
{
489+
"command": "intersystems-community.servermanager.migratePasswords",
490+
"when": "config.intersystemsServerManager.authentication.provider == intersystems-server-credentials"
491+
},
492+
{
493+
"command": "intersystems-community.servermanager.storePassword",
494+
"when": "false"
495+
},
496+
{
497+
"command": "intersystems-community.servermanager.clearPassword",
498+
"when": "false"
499+
},
482500
{
483501
"command": "intersystems-community.servermanager.addToStarred",
484502
"when": "false"
@@ -644,12 +662,12 @@
644662
},
645663
{
646664
"command": "intersystems-community.servermanager.storePassword",
647-
"when": "view == intersystems-community_servermanager && viewItem =~ /\\.server\\./",
665+
"when": "view == intersystems-community_servermanager && viewItem =~ /\\.server\\./ && config.intersystemsServerManager.authentication.provider != intersystems-server-credentials",
648666
"group": "2_password@10"
649667
},
650668
{
651669
"command": "intersystems-community.servermanager.clearPassword",
652-
"when": "view == intersystems-community_servermanager && viewItem =~ /\\.server\\./",
670+
"when": "view == intersystems-community_servermanager && viewItem =~ /\\.server\\./ && config.intersystemsServerManager.authentication.provider != intersystems-server-credentials",
653671
"group": "2_password@20"
654672
}
655673
]

src/commands/managePasswords.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as vscode from "vscode";
22
import { getServerNames } from "../api/getServerNames";
33
import { credentialCache } from "../api/getServerSpec";
4+
import { ServerManagerAuthenticationProvider } from "../authenticationProvider";
45
import { extensionId } from "../extension";
56
import { Keychain } from "../keychain";
67
import { ServerTreeItem } from "../ui/serverManagerView";
@@ -60,6 +61,78 @@ export async function clearPassword(treeItem?: ServerTreeItem): Promise<string>
6061
return reply;
6162
}
6263

64+
interface IMigratePasswordItem extends vscode.QuickPickItem {
65+
serverName: string;
66+
userName: string;
67+
password: string;
68+
};
69+
70+
export async function migratePasswords(secretStorage: vscode.SecretStorage): Promise<void> {
71+
const credentials = await Keychain.findCredentials();
72+
if (credentials.length === 0) {
73+
vscode.window.showInformationMessage('No legacy stored passwords found.');
74+
} else {
75+
76+
// Collect only those for which server definition exists with a username
77+
// and no credentials yet stored in our SecretStorage
78+
const migratableCredentials: IMigratePasswordItem[] = [];
79+
(await Promise.all(
80+
credentials.map(async (item): Promise<IMigratePasswordItem | undefined> => {
81+
const serverName = item.account;
82+
const username: string | undefined = vscode.workspace.getConfiguration("intersystems.servers." + serverName).get("username");
83+
if (!username) {
84+
return undefined;
85+
}
86+
if (username === "" || username === "UnknownUser") {
87+
return undefined;
88+
}
89+
const sessionId = ServerManagerAuthenticationProvider.sessionId(serverName, username);
90+
const credentialKey = ServerManagerAuthenticationProvider.credentialKey(sessionId);
91+
return (await secretStorage.get(credentialKey) ? undefined : {label: `${serverName} (${username})`, picked: true, serverName, userName: username, password: item.password});
92+
})
93+
))
94+
.forEach((item) => {
95+
if (item) {
96+
migratableCredentials.push(item);
97+
}
98+
});
99+
100+
if (migratableCredentials.length === 0) {
101+
const message = 'No remaining legacy stored passwords are eligible for migration.';
102+
const detail = 'They are either for servers with a password already stored in the new format, or for servers whose definition does not specify a username.'
103+
await vscode.window.showWarningMessage(message,
104+
{modal: true, detail}
105+
);
106+
} else {
107+
const choices = await vscode.window.showQuickPick<IMigratePasswordItem>(migratableCredentials,
108+
{ canPickMany: true,
109+
title: "Migrate Server Manager legacy stored passwords",
110+
placeHolder: "Select connections whose passwords you want to migrate"
111+
}
112+
)
113+
if (!choices) {
114+
return;
115+
} else if (choices.length > 0) {
116+
await Promise.all(choices.map(async (item) => {
117+
const sessionId = ServerManagerAuthenticationProvider.sessionId(item.serverName, item.userName);
118+
const credentialKey = ServerManagerAuthenticationProvider.credentialKey(sessionId);
119+
return secretStorage.store(credentialKey, item.password);
120+
}));
121+
vscode.window.showInformationMessage(`Migrated ${choices.length} ${choices.length > 1 ? "passwords" : "password"}.`);
122+
}
123+
}
124+
const detail = "Do this to tidy up your keystore once you have migrated passwords and will not be reverting to an earlier Server Manager.";
125+
if ((await vscode.window.showInformationMessage(`Delete all legacy stored passwords?`, {modal: true, detail}, {title: "Yes"}, {title: "No", isCloseAffordance: true}))?.title === "Yes") {
126+
await Promise.all(credentials.map(async (item) => {
127+
const keychain = new Keychain(item.account);
128+
return keychain.deletePassword()
129+
}));
130+
vscode.window.showInformationMessage(`Deleted ${credentials.length} ${credentials.length > 1 ? "passwords" : "password"}.`);
131+
}
132+
}
133+
return;
134+
}
135+
63136
async function commonPickServer(options?: vscode.QuickPickOptions): Promise<string | undefined> {
64137
// Deliberately uses its own API to illustrate how other extensions would
65138
const serverManagerExtension = vscode.extensions.getExtension(extensionId);

src/extension.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { getServerSummary } from "./api/getServerSummary";
99
import { pickServer } from "./api/pickServer";
1010
import { AUTHENTICATION_PROVIDER, ServerManagerAuthenticationProvider } from "./authenticationProvider";
1111
import { importFromRegistry } from "./commands/importFromRegistry";
12-
import { clearPassword, storePassword } from "./commands/managePasswords";
12+
import { clearPassword, migratePasswords, storePassword } from "./commands/managePasswords";
1313
import { cookieJar } from "./makeRESTRequest";
1414
import { NamespaceTreeItem, ProjectTreeItem, ServerManagerView, ServerTreeItem, SMTreeItem } from "./ui/serverManagerView";
1515

@@ -269,6 +269,11 @@ export function activate(context: vscode.ExtensionContext) {
269269
});
270270
}),
271271
);
272+
context.subscriptions.push(
273+
vscode.commands.registerCommand(`${extensionId}.migratePasswords`, async () => {
274+
await migratePasswords(context.secrets);
275+
}),
276+
);
272277
context.subscriptions.push(
273278
vscode.commands.registerCommand(`${extensionId}.setIconRed`, (server?: ServerTreeItem) => {
274279
if (server?.name) {

src/keychain.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,16 @@ export interface IKeytar {
1818
getPassword: typeof keytarType["getPassword"];
1919
setPassword: typeof keytarType["setPassword"];
2020
deletePassword: typeof keytarType["deletePassword"];
21+
findCredentials: typeof keytarType["findCredentials"];
22+
}
23+
24+
function serviceId(): string {
25+
return `${vscode.env.uriScheme}-${extensionId}:password`;
2126
}
2227

2328
export class Keychain {
29+
2430
private keytar: IKeytar;
25-
private serviceId: string;
2631
private accountId: string;
2732

2833
constructor(connectionName: string) {
@@ -32,13 +37,12 @@ export class Keychain {
3237
}
3338

3439
this.keytar = keytar;
35-
this.serviceId = `${vscode.env.uriScheme}-${extensionId}:password`;
3640
this.accountId = connectionName;
3741
}
3842

3943
public async setPassword(password: string): Promise<void> {
4044
try {
41-
return await this.keytar.setPassword(this.serviceId, this.accountId, password);
45+
return await this.keytar.setPassword(serviceId(), this.accountId, password);
4246
} catch (e) {
4347
// Ignore
4448
await vscode.window.showErrorMessage(
@@ -50,7 +54,7 @@ export class Keychain {
5054

5155
public async getPassword(): Promise<string | null | undefined> {
5256
try {
53-
return await this.keytar.getPassword(this.serviceId, this.accountId);
57+
return await this.keytar.getPassword(serviceId(), this.accountId);
5458
} catch (e) {
5559
// Ignore
5660
logger.error(`Getting password failed: ${e}`);
@@ -60,11 +64,27 @@ export class Keychain {
6064

6165
public async deletePassword(): Promise<boolean | undefined> {
6266
try {
63-
return await this.keytar.deletePassword(this.serviceId, this.accountId);
67+
return await this.keytar.deletePassword(serviceId(), this.accountId);
6468
} catch (e) {
6569
// Ignore
6670
logger.error(`Deleting password failed: ${e}`);
6771
return Promise.resolve(undefined);
6872
}
6973
}
74+
75+
public static async findCredentials(): Promise<{account: string; password: string}[]> {
76+
console.log(serviceId());
77+
const keytar = getKeytar();
78+
try {
79+
if (!keytar) {
80+
throw new Error("System keychain unavailable");
81+
}
82+
return await keytar.findCredentials(serviceId());
83+
} catch (e) {
84+
// Ignore
85+
logger.error(`Finding credentials failed: ${e}`);
86+
return Promise.resolve([]);
87+
}
88+
89+
}
7090
}

0 commit comments

Comments
 (0)