Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 2 additions & 4 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ trigger:
# https://docs.microsoft.com/en-us/azure/devops/pipelines/agents/hosted?view=azure-devops
strategy:
matrix:
# # Linux testing is currently disabled because of issues with
# # headless linux & keytar. Tracked: VSCODE-110
# linux:
# imageName: 'ubuntu-latest'
linux:
imageName: 'ubuntu-latest'
mac:
imageName: 'macos-latest'
windows:
Expand Down
53 changes: 18 additions & 35 deletions src/connectionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,31 @@ import { v4 as uuidv4 } from 'uuid';
import * as vscode from 'vscode';
import Connection = require('mongodb-connection-model/lib/model');
import DataService = require('mongodb-data-service');
import * as keytarType from 'keytar';

import { ConnectionModelType } from './connectionModelType';
import { DataServiceType } from './dataServiceType';
import { createLogger } from './logging';
import { StatusView } from './views';
import { EventEmitter } from 'events';
import { StorageController, StorageVariables } from './storage';
import { SavedConnection, StorageScope } from './storage/storageController';
import { getNodeModule } from './utils/getNodeModule';
import TelemetryController from './telemetry/telemetryController';
import { ext } from './extensionConstants';

const { name, version } = require('../package.json');
const log = createLogger('connection controller');
const MAX_CONNECTION_NAME_LENGTH = 512;

type KeyTar = typeof keytarType;

export enum DataServiceEventTypes {
CONNECTIONS_DID_CHANGE = 'CONNECTIONS_DID_CHANGE',
ACTIVE_CONNECTION_CHANGED = 'ACTIVE_CONNECTION_CHANGED',
ACTIVE_CONNECTION_CHANGING = 'ACTIVE_CONNECTION_CHANGING'
ACTIVE_CONNECTION_CHANGING = 'ACTIVE_CONNECTION_CHANGING',
}

export enum ConnectionTypes {
CONNECTION_FORM = 'CONNECTION_FORM',
CONNECTION_STRING = 'CONNECTION_STRING',
CONNECTION_ID = 'CONNECTION_ID'
CONNECTION_ID = 'CONNECTION_ID',
}

export type SavedConnectionInformation = {
Expand All @@ -48,8 +46,6 @@ export default class ConnectionController {
} = {};

private readonly _serviceName = 'mdb.vscode.savedConnections';
private _keytar: KeyTar | undefined;

_activeDataService: null | DataServiceType = null;
_activeConnectionModel: null | ConnectionModelType = null;
private _currentConnectionId: null | string = null;
Expand All @@ -73,33 +69,20 @@ export default class ConnectionController {
this._statusView = _statusView;
this._storageController = storageController;
this._telemetryController = telemetryController;

try {
// We load keytar in two different ways. This is because when the
// extension is webpacked it requires the vscode external keytar dependency
// differently then our testing development environment.
this._keytar = require('keytar');

if (!this._keytar) {
this._keytar = getNodeModule<typeof keytarType>('keytar');
}
} catch (err) {
// Couldn't load keytar, proceed without storing & loading connections.
}
}

_loadSavedConnection = async (
connectionId: string,
savedConnection: SavedConnection
): Promise<void> => {
if (!this._keytar) {
if (!ext.keytarModule) {
return;
}

let loadedSavedConnection: LoadedConnection;

try {
const unparsedConnectionInformation = await this._keytar.getPassword(
const unparsedConnectionInformation = await ext.keytarModule.getPassword(
this._serviceName,
connectionId
);
Expand Down Expand Up @@ -138,7 +121,7 @@ export default class ConnectionController {
};

loadSavedConnections = async (): Promise<void> => {
if (!this._keytar) {
if (!ext.keytarModule) {
return;
}

Expand Down Expand Up @@ -297,10 +280,10 @@ export default class ConnectionController {

this._connections[connectionId] = newLoadedConnection;

if (this._keytar) {
if (ext.keytarModule) {
const connectionInfoAsString = JSON.stringify(connectionInformation);

await this._keytar.setPassword(
await ext.keytarModule.setPassword(
this._serviceName,
connectionId,
connectionInfoAsString
Expand Down Expand Up @@ -496,8 +479,8 @@ export default class ConnectionController {
): Promise<void> => {
delete this._connections[connectionId];

if (this._keytar) {
await this._keytar.deletePassword(this._serviceName, connectionId);
if (ext.keytarModule) {
await ext.keytarModule.deletePassword(this._serviceName, connectionId);
// We only remove the connection from the saved connections if we
// have deleted the connection information with keytar.
this._storageController.removeConnection(connectionId);
Expand Down Expand Up @@ -595,13 +578,13 @@ export default class ConnectionController {
const connectionNameToRemove:
| string
| undefined = await vscode.window.showQuickPick(
connectionIds.map(
(id, index) => `${index + 1}: ${this._connections[id].name}`
),
{
placeHolder: 'Choose a connection to remove...'
}
);
connectionIds.map(
(id, index) => `${index + 1}: ${this._connections[id].name}`
),
{
placeHolder: 'Choose a connection to remove...'
}
);

if (!connectionNameToRemove) {
return Promise.resolve(false);
Expand Down
8 changes: 8 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import * as vscode from 'vscode';

import { ext } from './extensionConstants';
import { createKeytar } from './utils/keytar';
import { createLogger } from './logging';
const log = createLogger('extension.ts');

Expand All @@ -26,6 +27,13 @@ export function activate(context: vscode.ExtensionContext): void {
log.info('activate extension called');

ext.context = context;

try {
ext.keytarModule = createKeytar();
} catch (err) {
// Couldn't load keytar, proceed without storing & loading connections.
}

mdbExtension = new MDBExtensionController(context);
mdbExtension.activate();

Expand Down
2 changes: 2 additions & 0 deletions src/extensionConstants.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { ExtensionContext } from 'vscode';
import { KeytarInterface } from './utils/keytar';

// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace ext {
export let context: ExtensionContext;
export let keytarModule: KeytarInterface | undefined;
}

export function getImagesPath(): string {
Expand Down
41 changes: 12 additions & 29 deletions src/test/suite/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ import path = require('path');
import * as keytarType from 'keytar';

import MDBExtensionController from '../../mdbExtensionController';
import { ext } from '../../extensionConstants';
import KeytarStub from './keytarStub';
import { TestExtensionContext } from './stubs';
import { mdbTestExtension } from './stubbableMdbExtension';

type KeyTar = typeof keytarType;

export function run(): Promise<void> {
const reporterOptions = {
spec: '-',
Expand Down Expand Up @@ -37,46 +41,25 @@ export function run(): Promise<void> {
);
mdbTestExtension.testExtensionController.activate();

// We avoid using the user's credential store when running tests
// in order to ensure we're not polluting the credential store
// and because its tough to get the credential store running on
// headless linux.
ext.keytarModule = new KeytarStub();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏


// Disable metrics.
vscode.workspace.getConfiguration('mdb').update('sendTelemetry', false);

// Disable the dialogue for prompting the user where to store the connection.
vscode.workspace
.getConfiguration('mdb.connectionSaving')
.update('hideOptionToChooseWhereToSaveNewConnections', true)
.then(async () => {
// We require keytar in runtime because it is a vscode provided
// native node module.
const keytar: typeof keytarType = require('keytar');
const existingCredentials = await keytar.findCredentials(
'mdb.vscode.savedConnections'
);

.then(() => {
// Add files to the test suite.
files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f)));
try {
// Run the mocha test.
mocha.run(async (failures) => {
// After tests are run we clear any passwords added
// to local secure storage.
const postRunCredentials = await keytar.findCredentials(
'mdb.vscode.savedConnections'
);
postRunCredentials.forEach((credential) => {
if (
!existingCredentials.find(
(existingCredential) =>
existingCredential.account === credential.account
)
) {
// If the credential is newly added, we remove it.
keytar.deletePassword(
'mdb.vscode.savedConnections',
credential.account
);
}
});

mocha.run((failures) => {
if (failures > 0) {
e(new Error(`${failures} tests failed.`));
} else {
Expand Down
63 changes: 63 additions & 0 deletions src/test/suite/keytarStub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { KeytarInterface } from '../../utils/keytar';

const retrievalDelay = 1; // ms simulated delay on keytar methods.

export default class KeytarStub implements KeytarInterface {
private _services: Map<string, Map<string, string>> = new Map<string, Map<string, string>>();

public async findCredentials(service: string): Promise<Map<string, string> | undefined> {
await this.delay();
const savedServices = this._services.get(service);
if (savedServices) {
return savedServices;
}

return undefined;
}

public async getPassword(service: string, account: string): Promise<string | null> {
await this.delay();
const savedService = this._services.get(service);
if (savedService) {
const savedAccount = savedService.get(account);

if (savedAccount !== undefined) {
return savedAccount;
}
}

return null;
}

public async setPassword(service: string, account: string, password: string): Promise<void> {
await this.delay();
let savedService = this._services.get(service);
if (!savedService) {
savedService = new Map<string, string>();
this._services.set(service, savedService);
}

savedService.set(account, password);
}

public async deletePassword(service: string, account: string): Promise<boolean> {
await this.delay();
const savedService = this._services.get(service);
if (savedService) {
if (savedService.has(account)) {
savedService.delete(account);
return true;
}
}

return false;
}

private async delay(): Promise<void> {
return new Promise<void>(resolve => {
setTimeout(() => {
resolve();
}, retrievalDelay);
});
}
}
53 changes: 53 additions & 0 deletions src/utils/keytar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as keytarType from 'keytar';

import { getNodeModule } from './getNodeModule';

export interface KeytarInterface {
/**
* Get the stored password for the service and account.
*
* @param service The string service name.
* @param account The string account name.
*
* @returns A promise for the password string.
*/
getPassword(service: string, account: string): Promise<string | null>;

/**
* Add the password for the service and account to the keychain.
*
* @param service The string service name.
* @param account The string account name.
* @param password The string password.
*
* @returns A promise for the set password completion.
*/
setPassword(
service: string,
account: string,
password: string
): Promise<void>;

/**
* Delete the stored password for the service and account.
*
* @param service The string service name.
* @param account The string account name.
*
* @returns A promise for the deletion status. True on success.
*/
deletePassword(service: string, account: string): Promise<boolean>;
}

export const createKeytar = (): KeytarInterface | undefined => {
// We load keytar in two different ways. This is because when the
// extension is webpacked it requires the vscode external keytar dependency
// differently then our development environment.
let keytarModule: KeytarInterface | undefined = require('keytar');

if (!keytarModule) {
keytarModule = getNodeModule<typeof keytarType>('keytar');
}

return keytarModule;
};