-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #9 from gjsjohnmurray/fix-7
Fix #7 - Use host keychain for password storage
- Loading branch information
Showing
11 changed files
with
893 additions
and
8 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import * as vscode from 'vscode'; | ||
import { ServerName, ServerSpec } from '../extension'; | ||
|
||
export function getServerNames(scope?: vscode.ConfigurationScope): ServerName[] { | ||
let names: ServerName[] = []; | ||
const servers = vscode.workspace.getConfiguration('intersystems', scope).get('servers'); | ||
|
||
if (typeof servers === 'object' && servers) { | ||
const defaultName: string = servers['/default'] || ''; | ||
if (defaultName.length > 0 && servers[defaultName]) { | ||
names.push({ | ||
name: defaultName, | ||
description: `${servers[defaultName].description || ''} (default)`, | ||
detail: serverDetail(servers[defaultName]) | ||
}); | ||
} | ||
for (const key in servers) { | ||
if (!key.startsWith('/') && key !== defaultName) { | ||
names.push({ | ||
name: key, | ||
description: servers[key].description || '', | ||
detail: serverDetail(servers[key]) | ||
}); | ||
} | ||
} | ||
} | ||
return names; | ||
} | ||
|
||
function serverDetail(connSpec: ServerSpec): string { | ||
return `${connSpec.webServer.scheme || 'http'}://${connSpec.webServer.host}:${connSpec.webServer.port}/${connSpec.webServer.pathPrefix || ''}`; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import * as vscode from 'vscode'; | ||
import { ServerSpec } from '../extension'; | ||
import { Keychain } from '../keychain'; | ||
|
||
interface CredentialSet { | ||
username: string, | ||
password: string | ||
} | ||
|
||
export let credentialCache = new Map<string, CredentialSet>(); | ||
|
||
export async function getServerSpec(name: string, scope?: vscode.ConfigurationScope, flushCredentialCache: boolean = false): Promise<ServerSpec | undefined> { | ||
if (flushCredentialCache) { | ||
credentialCache[name] = undefined; | ||
} | ||
let server: ServerSpec | undefined = vscode.workspace.getConfiguration('intersystems.servers', scope).get(name); | ||
|
||
// Unknown server | ||
if (!server) { | ||
return undefined; | ||
} | ||
|
||
server.name = name; | ||
server.description = server.description || ''; | ||
server.webServer.scheme = server.webServer.scheme || 'http'; | ||
server.webServer.pathPrefix = server.webServer.pathPrefix || ''; | ||
|
||
// Obtain a username (including blank to try connecting anonymously) | ||
if (!server.username) { | ||
await vscode.window | ||
.showInputBox({ | ||
placeHolder: `Username to connect to InterSystems server '${name}' as`, | ||
prompt: 'Leave empty to attempt unauthenticated access', | ||
ignoreFocusOut: true, | ||
}) | ||
.then((username) => { | ||
if (username && server) { | ||
server.username = username; | ||
} else { | ||
return undefined; | ||
} | ||
}); | ||
if (!server.username) { | ||
server.username = ''; | ||
server.password = ''; | ||
} | ||
} | ||
|
||
// Obtain password from session cache or keychain unless trying to connect anonymously | ||
if (server.username && !server.password) { | ||
if (credentialCache[name] && credentialCache[name].username === server.username) { | ||
server.password = credentialCache[name]; | ||
} else { | ||
const keychain = new Keychain(name); | ||
const password = await keychain.getPassword().then(result => { | ||
if (typeof result === 'string') { | ||
return result; | ||
} else { | ||
return undefined; | ||
} | ||
}); | ||
if (password) { | ||
server.password = password; | ||
credentialCache[name] = {username: server.username, password: password}; | ||
} | ||
} | ||
|
||
} | ||
if (server.username && !server.password) { | ||
await vscode.window | ||
.showInputBox({ | ||
password: true, | ||
placeHolder: `Password for user '${server.username}' on InterSystems server '${name}'`, | ||
validateInput: (value => { | ||
return value.length > 0 ? '' : 'Mandatory field'; | ||
}), | ||
ignoreFocusOut: true, | ||
}) | ||
.then((password) => { | ||
if (password && server) { | ||
server.password = password; | ||
credentialCache[name] = {username: server.username, password: password}; | ||
} else { | ||
server = undefined; | ||
} | ||
}) | ||
} | ||
return server; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import * as vscode from 'vscode'; | ||
import { getServerNames } from './getServerNames'; | ||
|
||
export async function pickServer(scope?: vscode.ConfigurationScope, options: vscode.QuickPickOptions = {}): Promise<string | undefined> { | ||
const names = getServerNames(scope); | ||
|
||
let qpItems: vscode.QuickPickItem[] = []; | ||
|
||
options.matchOnDescription = options?.matchOnDescription || true; | ||
options.placeHolder = options?.placeHolder || 'Pick an InterSystems server'; | ||
options.canPickMany = false; | ||
|
||
names.forEach(element => { | ||
qpItems.push({label: element.name, description: element.description, detail: options?.matchOnDetail ? element.detail : undefined}); | ||
}); | ||
return await vscode.window.showQuickPick(qpItems, options).then(item => { | ||
return item ? item.label : undefined; | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import * as vscode from 'vscode'; | ||
import { extensionId } from '../extension'; | ||
import { Keychain } from '../keychain'; | ||
import { credentialCache } from '../api/getServerSpec'; | ||
|
||
export async function storePassword() { | ||
const name = await commonPickServer({matchOnDetail: true}); | ||
if (name) { | ||
await vscode.window | ||
.showInputBox({ | ||
password: true, | ||
placeHolder: 'Password to store in keychain', | ||
prompt: `For connection to InterSystems server '${name}'`, | ||
validateInput: (value => { | ||
return value.length > 0 ? '' : 'Mandatory field'; | ||
}), | ||
ignoreFocusOut: true, | ||
}) | ||
.then((password) => { | ||
if (password) { | ||
credentialCache[name] = undefined; | ||
new Keychain(name).setPassword(password).then(() => { | ||
vscode.window.showInformationMessage(`Password for '${name}' stored in keychain.`); | ||
}); | ||
} | ||
}) | ||
|
||
} | ||
} | ||
|
||
export async function clearPassword() { | ||
const name = await commonPickServer({matchOnDetail: true}); | ||
if (name) { | ||
credentialCache[name] = undefined; | ||
const keychain = new Keychain(name); | ||
if (!await keychain.getPassword()) { | ||
vscode.window.showWarningMessage(`No password for '${name}' found in keychain.`); | ||
} else if (await keychain.deletePassword()) { | ||
vscode.window.showInformationMessage(`Password for '${name}' removed from keychain.`); | ||
} else { | ||
vscode.window.showWarningMessage(`Failed to remove password for '${name}' from keychain.`); | ||
} | ||
} | ||
} | ||
|
||
async function commonPickServer(options?: vscode.QuickPickOptions): Promise<string | undefined> { | ||
// Deliberately uses its own API to illustrate how other extensions would | ||
const serverManagerExtension = vscode.extensions.getExtension(extensionId); | ||
if (!serverManagerExtension) { | ||
vscode.window.showErrorMessage(`Extension '${extensionId}' is not installed, or has been disabled.`) | ||
return; | ||
} | ||
if (!serverManagerExtension.isActive) { | ||
serverManagerExtension.activate(); | ||
} | ||
const myApi = serverManagerExtension.exports; | ||
|
||
return await myApi.pickServer(undefined, options); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import * as vscode from 'vscode'; | ||
import { extensionId, ServerSpec } from '../extension'; | ||
|
||
export async function testPickServer() { | ||
await commonTestPickServer(); | ||
} | ||
|
||
export async function testPickServerWithoutCachedCredentials() { | ||
await commonTestPickServer(undefined, true); | ||
} | ||
|
||
export async function testPickServerDetailed() { | ||
await commonTestPickServer({matchOnDetail: true}); | ||
} | ||
|
||
async function commonTestPickServer(options?: vscode.QuickPickOptions, flushCredentialCache: boolean = false) { | ||
// Deliberately uses its own API in the same way as other extensions would | ||
const serverManagerExtension = vscode.extensions.getExtension(extensionId); | ||
if (!serverManagerExtension) { | ||
vscode.window.showErrorMessage(`Extension '${extensionId}' is not installed, or has been disabled.`) | ||
return | ||
} | ||
if (!serverManagerExtension.isActive) { | ||
serverManagerExtension.activate(); | ||
} | ||
const myApi = serverManagerExtension.exports; | ||
|
||
const name: string = await myApi.pickServer(undefined, options); | ||
if (name) { | ||
const connSpec: ServerSpec = await myApi.getServerSpec(name, undefined, flushCredentialCache); | ||
if (connSpec) { | ||
vscode.window.showInformationMessage(`Picked server '${connSpec.name}' at ${connSpec.webServer.scheme}://${connSpec.webServer.host}:${connSpec.webServer.port}/${connSpec.webServer.pathPrefix} ${!connSpec.username ? 'with unauthenticated access' : 'as user ' + connSpec.username }.`, 'OK'); | ||
} | ||
} | ||
} |
Oops, something went wrong.