diff --git a/src/sql/data.d.ts b/src/sql/data.d.ts index d797f402e6..06f6f1d8e3 100644 --- a/src/sql/data.d.ts +++ b/src/sql/data.d.ts @@ -71,6 +71,43 @@ declare module 'data' { export function registerProvider(provider: SerializationProvider): vscode.Disposable; } + /** + * Namespace for connection management + */ + export namespace connection { + /** + * Get the current connection based on the active editor or Object Explorer selection + */ + export function getCurrentConnection(): Thenable; + + /** + * Get all active connections + */ + export function getActiveConnections(): Thenable; + + /** + * Get the credentials for an active connection + * @param {string} connectionId The id of the connection + * @returns {{ [name: string]: string}} A dictionary containing the credentials as they would be included in the connection's options dictionary + */ + export function getCredentials(connectionId: string): Thenable<{ [name: string]: string }>; + + /** + * Interface for representing a connection when working with connection APIs + */ + export interface Connection extends ConnectionInfo { + /** + * The name of the provider managing the connection (e.g. MSSQL) + */ + providerName: string; + + /** + * A unique identifier for the connection + */ + connectionId: string; + } + } + // EXPORTED INTERFACES ///////////////////////////////////////////////// export interface ConnectionInfo { diff --git a/src/sql/parts/connection/common/connectionManagement.ts b/src/sql/parts/connection/common/connectionManagement.ts index 8b880b6e2c..3590f69358 100644 --- a/src/sql/parts/connection/common/connectionManagement.ts +++ b/src/sql/parts/connection/common/connectionManagement.ts @@ -253,6 +253,21 @@ export interface IConnectionManagementService { * Refresh the IntelliSense cache for the connection with the given URI */ rebuildIntelliSenseCache(uri: string): Thenable; + + /** + * Get a copy of the connection profile with its passwords removed + * @param {IConnectionProfile} profile The connection profile to remove passwords from + * @returns {IConnectionProfile} A copy of the connection profile with passwords removed + */ + removeConnectionProfileCredentials(profile: IConnectionProfile): IConnectionProfile; + + /** + * Get the credentials for a connected connection profile, as they would appear in the options dictionary + * @param {string} profileId The id of the connection profile to get the password for + * @returns {{ [name: string]: string }} A dictionary containing the credentials as they would be included + * in the connection profile's options dictionary, or undefined if the profile is not connected + */ + getActiveConnectionCredentials(profileId: string): { [name: string]: string }; } export const IConnectionDialogService = createDecorator('connectionDialogService'); diff --git a/src/sql/parts/connection/common/connectionManagementService.ts b/src/sql/parts/connection/common/connectionManagementService.ts index cd7ca4ae3f..643e6b5429 100644 --- a/src/sql/parts/connection/common/connectionManagementService.ts +++ b/src/sql/parts/connection/common/connectionManagementService.ts @@ -56,6 +56,7 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IStatusbarService } from 'vs/platform/statusbar/common/statusbar'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { Deferred } from 'sql/base/common/promise'; +import { ConnectionOptionSpecialType } from 'sql/workbench/api/common/sqlExtHostTypes'; export class ConnectionManagementService implements IConnectionManagementService { @@ -659,7 +660,7 @@ export class ConnectionManagementService implements IConnectionManagementService } public getActiveConnections(): ConnectionProfile[] { - return this._connectionStore.getActiveConnections(); + return this._connectionStatusManager.getActiveConnectionProfiles(); } public saveProfileGroup(profile: IConnectionProfileGroup): Promise { @@ -1351,4 +1352,26 @@ export class ConnectionManagementService implements IConnectionManagementService this._editorGroupService.refreshEditorTitles(); } } + + public removeConnectionProfileCredentials(originalProfile: IConnectionProfile): IConnectionProfile { + return this._connectionStore.getProfileWithoutPassword(originalProfile); + } + + public getActiveConnectionCredentials(profileId: string): { [name: string]: string } { + let profile = this.getActiveConnections().find(connectionProfile => connectionProfile.id === profileId); + if (!profile) { + return undefined; + } + + // Find the password option for the connection provider + let passwordOption = this._capabilitiesService.getCapabilities().find(capability => capability.providerName === profile.providerName).connectionProvider.options.find( + option => option.specialValueType === ConnectionOptionSpecialType.password); + if (!passwordOption) { + return undefined; + } + + let credentials = {}; + credentials[passwordOption.name] = profile.options[passwordOption.name]; + return credentials; + } } diff --git a/src/sql/parts/connection/common/connectionStatusManager.ts b/src/sql/parts/connection/common/connectionStatusManager.ts index 4199e6dce1..768a807cc3 100644 --- a/src/sql/parts/connection/common/connectionStatusManager.ts +++ b/src/sql/parts/connection/common/connectionStatusManager.ts @@ -209,4 +209,13 @@ export class ConnectionStatusManager { } return providerId; } + + /** + * Get a list of the active connection profiles managed by the status manager + */ + public getActiveConnectionProfiles(): ConnectionProfile[] { + let profiles = Object.values(this._connections).map((connectionInfo: ConnectionManagementInfo) => connectionInfo.connectionProfile); + // Remove duplicate profiles that may be listed multiple times under different URIs by filtering for profiles that don't have the same ID as an earlier profile in the list + return profiles.filter((profile, index) => profiles.findIndex(otherProfile => otherProfile.id === profile.id) === index); + } } \ No newline at end of file diff --git a/src/sql/parts/connection/common/connectionStore.ts b/src/sql/parts/connection/common/connectionStore.ts index 58c09e179f..62450d2507 100644 --- a/src/sql/parts/connection/common/connectionStore.ts +++ b/src/sql/parts/connection/common/connectionStore.ts @@ -19,6 +19,7 @@ import { ConfigurationEditingService } from 'vs/workbench/services/configuration import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService'; import * as data from 'data'; +import { ConnectionOptionSpecialType } from 'sql/workbench/api/common/sqlExtHostTypes'; const MAX_CONNECTIONS_DEFAULT = 25; diff --git a/src/sql/workbench/api/node/extHostConnectionManagement.ts b/src/sql/workbench/api/node/extHostConnectionManagement.ts new file mode 100644 index 0000000000..f92226b5b8 --- /dev/null +++ b/src/sql/workbench/api/node/extHostConnectionManagement.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { IThreadService } from 'vs/workbench/services/thread/common/threadService'; +import { ExtHostConnectionManagementShape, SqlMainContext, MainThreadConnectionManagementShape } from 'sql/workbench/api/node/sqlExtHost.protocol'; +import * as data from 'data'; + +export class ExtHostConnectionManagement extends ExtHostConnectionManagementShape { + + private _proxy: MainThreadConnectionManagementShape; + + constructor( + threadService: IThreadService + ) { + super(); + this._proxy = threadService.get(SqlMainContext.MainThreadConnectionManagement); + } + + public $getActiveConnections(): Thenable { + return this._proxy.$getActiveConnections(); + } + + public $getCurrentConnection(): Thenable { + return this._proxy.$getCurrentConnection(); + } + + public $getCredentials(connectionId: string): Thenable<{ [name: string]: string}> { + return this._proxy.$getCredentials(connectionId); + } +} diff --git a/src/sql/workbench/api/node/mainThreadConnectionManagement.ts b/src/sql/workbench/api/node/mainThreadConnectionManagement.ts new file mode 100644 index 0000000000..56e2e8f6bb --- /dev/null +++ b/src/sql/workbench/api/node/mainThreadConnectionManagement.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { SqlExtHostContext, SqlMainContext, ExtHostConnectionManagementShape, MainThreadConnectionManagementShape } from 'sql/workbench/api/node/sqlExtHost.protocol'; +import * as data from 'data'; +import { IExtHostContext } from 'vs/workbench/api/node/extHost.protocol'; +import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; +import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement'; +import { IObjectExplorerService } from 'sql/parts/registeredServer/common/objectExplorerService'; +import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import * as TaskUtilities from 'sql/workbench/common/taskUtilities'; +import { IConnectionProfile } from 'sql/parts/connection/common/interfaces'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; + +@extHostNamedCustomer(SqlMainContext.MainThreadConnectionManagement) +export class MainThreadConnectionManagement implements MainThreadConnectionManagementShape { + + private _proxy: ExtHostConnectionManagementShape; + private _toDispose: IDisposable[]; + + constructor( + extHostContext: IExtHostContext, + @IConnectionManagementService private _connectionManagementService: IConnectionManagementService, + @IObjectExplorerService private _objectExplorerService: IObjectExplorerService, + @IWorkbenchEditorService private _workbenchEditorService: IWorkbenchEditorService + ) { + if (extHostContext) { + this._proxy = extHostContext.get(SqlExtHostContext.ExtHostConnectionManagement); + } + this._toDispose = []; + } + + public dispose(): void { + this._toDispose = dispose(this._toDispose); + } + + public $getActiveConnections(): Thenable { + return Promise.resolve(this._connectionManagementService.getActiveConnections().map(profile => this.convertConnection(profile))); + } + + public $getCurrentConnection(): Thenable { + return Promise.resolve(this.convertConnection(TaskUtilities.getCurrentGlobalConnection(this._objectExplorerService, this._connectionManagementService, this._workbenchEditorService, true))); + } + + public $getCredentials(connectionId: string): Thenable<{ [name: string]: string }> { + return Promise.resolve(this._connectionManagementService.getActiveConnectionCredentials(connectionId)); + } + + private convertConnection(profile: IConnectionProfile): data.connection.Connection { + if (!profile) { + return undefined; + } + profile = this._connectionManagementService.removeConnectionProfileCredentials(profile); + let connection: data.connection.Connection = { + providerName: profile.providerName, + connectionId: profile.id, + options: profile.options + }; + return connection; + } +} diff --git a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts index aa5de092e6..49bdfb7689 100644 --- a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts +++ b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts @@ -28,6 +28,7 @@ import { ExtHostConfiguration } from 'vs/workbench/api/node/extHostConfiguration import { ExtHostModalDialogs } from 'sql/workbench/api/node/extHostModalDialog'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtensionApiFactory } from 'vs/workbench/api/node/extHost.api.impl'; +import { ExtHostConnectionManagement } from 'sql/workbench/api/node/extHostConnectionManagement'; export interface ISqlExtensionApiFactory { vsCodeFactory(extension: IExtensionDescription): typeof vscode; @@ -49,6 +50,7 @@ export function createApiFactory( // Addressable instances const extHostAccountManagement = threadService.set(SqlExtHostContext.ExtHostAccountManagement, new ExtHostAccountManagement(threadService)); + const extHostConnectionManagement = threadService.set(SqlExtHostContext.ExtHostConnectionManagement, new ExtHostConnectionManagement(threadService)); const extHostCredentialManagement = threadService.set(SqlExtHostContext.ExtHostCredentialManagement, new ExtHostCredentialManagement(threadService)); const extHostDataProvider = threadService.set(SqlExtHostContext.ExtHostDataProtocol, new ExtHostDataProtocol(threadService)); const extHostSerializationProvider = threadService.set(SqlExtHostContext.ExtHostSerializationProvider, new ExtHostSerializationProvider(threadService)); @@ -74,6 +76,19 @@ export function createApiFactory( } }; + // namespace: connection + const connection: typeof data.connection = { + getActiveConnections(): Thenable { + return extHostConnectionManagement.$getActiveConnections(); + }, + getCurrentConnection(): Thenable { + return extHostConnectionManagement.$getCurrentConnection(); + }, + getCredentials(connectionId: string): Thenable<{ [name: string]: string }> { + return extHostConnectionManagement.$getCredentials(connectionId); + } + }; + // namespace: credentials const credentials: typeof data.credentials = { registerProvider(provider: data.CredentialProvider): vscode.Disposable { @@ -246,6 +261,7 @@ export function createApiFactory( return { accounts, + connection, credentials, resources, serialization, diff --git a/src/sql/workbench/api/node/sqlExtHost.contribution.ts b/src/sql/workbench/api/node/sqlExtHost.contribution.ts index 0de9854c1c..8c3b70dc38 100644 --- a/src/sql/workbench/api/node/sqlExtHost.contribution.ts +++ b/src/sql/workbench/api/node/sqlExtHost.contribution.ts @@ -10,6 +10,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; // --- SQL contributions +import 'sql/workbench/api/node/mainThreadConnectionManagement'; import 'sql/workbench/api/node/mainThreadCredentialManagement'; import 'sql/workbench/api/node/mainThreadDataProtocol'; import 'sql/workbench/api/node/mainThreadSerializationProvider'; diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index a5025fb1ea..9fdd025582 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -23,6 +23,8 @@ export abstract class ExtHostAccountManagementShape { $refresh(handle: number, account: data.Account): Thenable { throw ni(); } } +export abstract class ExtHostConnectionManagementShape { } + export abstract class ExtHostDataProtocolShape { /** @@ -389,6 +391,12 @@ export interface MainThreadDataProtocolShape extends IDisposable { $onEditSessionReady(handle: number, ownerUri: string, success: boolean, message: string); } +export interface MainThreadConnectionManagementShape extends IDisposable { + $getActiveConnections(): Thenable; + $getCurrentConnection(): Thenable; + $getCredentials(connectionId: string): Thenable<{ [name: string]: string }>; +} + export interface MainThreadCredentialManagementShape extends IDisposable { $registerCredentialProvider(handle: number): TPromise; $unregisterCredentialProvider(handle: number): TPromise; @@ -406,6 +414,7 @@ function ni() { return new Error('Not implemented'); } export const SqlMainContext = { // SQL entries MainThreadAccountManagement: createMainId('MainThreadAccountManagement'), + MainThreadConnectionManagement: createMainId('MainThreadConnectionManagement'), MainThreadCredentialManagement: createMainId('MainThreadCredentialManagement'), MainThreadDataProtocol: createMainId('MainThreadDataProtocol'), MainThreadSerializationProvider: createMainId('MainThreadSerializationProvider'), @@ -415,6 +424,7 @@ export const SqlMainContext = { export const SqlExtHostContext = { ExtHostAccountManagement: createExtId('ExtHostAccountManagement'), + ExtHostConnectionManagement: createExtId('ExtHostConnectionManagement'), ExtHostCredentialManagement: createExtId('ExtHostCredentialManagement'), ExtHostDataProtocol: createExtId('ExtHostDataProtocol'), ExtHostSerializationProvider: createExtId('ExtHostSerializationProvider'), diff --git a/src/sql/workbench/common/taskUtilities.ts b/src/sql/workbench/common/taskUtilities.ts index 304c4ac27c..bb835e9eff 100644 --- a/src/sql/workbench/common/taskUtilities.ts +++ b/src/sql/workbench/common/taskUtilities.ts @@ -348,15 +348,20 @@ export function openInsight(query: IInsightsConfig, profile: IConnectionProfile, * Get the current global connection, which is the connection from the active editor, unless OE * is focused or there is no such editor, in which case it comes from the OE selection. Returns * undefined when there is no such connection. + * + * @param objectExplorerService + * @param connectionManagementService + * @param workbenchEditorService + * @param topLevelOnly If true, only return top-level (i.e. connected) Object Explorer connections instead of database connections when appropriate */ -export function getCurrentGlobalConnection(objectExplorerService: IObjectExplorerService, connectionManagementService: IConnectionManagementService, workbenchEditorService: IWorkbenchEditorService): IConnectionProfile { +export function getCurrentGlobalConnection(objectExplorerService: IObjectExplorerService, connectionManagementService: IConnectionManagementService, workbenchEditorService: IWorkbenchEditorService, topLevelOnly: boolean = false): IConnectionProfile { let connection: IConnectionProfile; let objectExplorerSelection = objectExplorerService.getSelectedProfileAndDatabase(); if (objectExplorerSelection) { let objectExplorerProfile = objectExplorerSelection.profile; if (connectionManagementService.isProfileConnected(objectExplorerProfile)) { - if (objectExplorerSelection.databaseName) { + if (objectExplorerSelection.databaseName && !topLevelOnly) { connection = objectExplorerProfile.cloneWithDatabase(objectExplorerSelection.databaseName); } else { connection = objectExplorerProfile; diff --git a/src/sqltest/parts/connection/connectionManagementService.test.ts b/src/sqltest/parts/connection/connectionManagementService.test.ts index 743075b1a1..1878822b49 100644 --- a/src/sqltest/parts/connection/connectionManagementService.test.ts +++ b/src/sqltest/parts/connection/connectionManagementService.test.ts @@ -790,4 +790,14 @@ suite('SQL ConnectionManagementService tests', () => { } }, err => done(err)); }); + + test('getActiveConnectionCredentials returns the credentials dictionary for a connection profile', () => { + let profile = Object.assign({}, connectionProfile); + profile.options = {password: profile.password}; + profile.id = 'test_id'; + connectionStatusManager.addConnection(profile, 'test_uri'); + (connectionManagementService as any)._connectionStatusManager = connectionStatusManager; + let credentials = connectionManagementService.getActiveConnectionCredentials(profile.id); + assert.equal(credentials['password'], profile.options['password']); + }); }); \ No newline at end of file diff --git a/src/sqltest/parts/connection/connectionStatusManager.test.ts b/src/sqltest/parts/connection/connectionStatusManager.test.ts index 6c7f4c3d43..9a44b2b14f 100644 --- a/src/sqltest/parts/connection/connectionStatusManager.test.ts +++ b/src/sqltest/parts/connection/connectionStatusManager.test.ts @@ -236,4 +236,19 @@ suite('SQL ConnectionStatusManager tests', () => { let connectionStatus = connections.getOriginalOwnerUri(connection2Id); assert.equal(connectionStatus, connection2Id); }); + + test('getActiveConnectionProfiles should return a list of all the unique connections that the status manager knows about', () => { + // Add duplicate connections + let newConnection = Object.assign({}, connectionProfile); + newConnection.id = 'test_id'; + newConnection.serverName = 'new_server_name'; + newConnection.options['databaseDisplayName'] = newConnection.databaseName; + connections.addConnection(newConnection, 'test_uri_1'); + connections.addConnection(newConnection, 'test_uri_2'); + + // Get the connections and verify that the duplicate is only returned once + let activeConnections = connections.getActiveConnectionProfiles(); + assert.equal(activeConnections.length, 4); + assert.equal(activeConnections.filter(connection => connection.matches(newConnection)).length, 1, 'Did not find newConnection in active connections'); + }); }); \ No newline at end of file diff --git a/src/sqltest/parts/connection/connectionStore.test.ts b/src/sqltest/parts/connection/connectionStore.test.ts index 869071db5c..52616b28ee 100644 --- a/src/sqltest/parts/connection/connectionStore.test.ts +++ b/src/sqltest/parts/connection/connectionStore.test.ts @@ -492,4 +492,18 @@ suite('SQL ConnectionStore tests', () => { actualGroup = connectionStore.getGroupFromId(childGroupId); assert.equal(actualGroup.id, childGroupId, 'Did not get the child group when looking it up with its ID'); }); + + test('getProfileWithoutPassword can return the profile without credentials in the password property or options dictionary', () => { + let connectionStore = new ConnectionStore(storageServiceMock.object, context.object, undefined, workspaceConfigurationServiceMock.object, + credentialStore.object, capabilitiesService.object, connectionConfig.object); + let profile = Object.assign({}, defaultNamedProfile); + profile.options['password'] = profile.password; + profile.id = 'testId'; + let expectedProfile = Object.assign({}, profile); + expectedProfile.password = ''; + expectedProfile.options['password'] = ''; + expectedProfile = ConnectionProfile.convertToConnectionProfile(msSQLCapabilities, expectedProfile).toIConnectionProfile(); + let profileWithoutCredentials = connectionStore.getProfileWithoutPassword(profile); + assert.deepEqual(profileWithoutCredentials.toIConnectionProfile(), expectedProfile); + }); }); \ No newline at end of file diff --git a/src/sqltest/stubs/connectionManagementService.test.ts b/src/sqltest/stubs/connectionManagementService.test.ts index c01a0b170d..37ea88ac8d 100644 --- a/src/sqltest/stubs/connectionManagementService.test.ts +++ b/src/sqltest/stubs/connectionManagementService.test.ts @@ -241,4 +241,12 @@ export class TestConnectionManagementService implements IConnectionManagementSer getTabColorForUri(uri: string): string { return undefined; } + + removeConnectionProfileCredentials(profile: IConnectionProfile): IConnectionProfile { + return undefined; + } + + getActiveConnectionCredentials(profileId: string): { [name: string]: string } { + return undefined; + } } \ No newline at end of file