Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a command line interface for connecting to a SQL Server #3047

Merged
merged 10 commits into from Oct 31, 2018
17 changes: 17 additions & 0 deletions src/sql/parts/commandLine/common/commandLine.ts
@@ -0,0 +1,17 @@
/*---------------------------------------------------------------------------------------------
* 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 { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export interface ICommandLineProcessing {
_serviceBrand: any;
/**
* Interprets the various Azure Data Studio-specific command line switches and
* performs the requisite tasks such as connecting to a server
*/
processCommandLine() : void;
}

export const ICommandLineProcessing = createDecorator<ICommandLineProcessing>('commandLineService');
77 changes: 77 additions & 0 deletions src/sql/parts/commandLine/common/commandLineService.ts
@@ -0,0 +1,77 @@
/*---------------------------------------------------------------------------------------------
* 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 { ConnectionProfile } from 'sql/parts/connection/common/connectionProfile';
import { ICommandLineProcessing } from 'sql/parts/commandLine/common/commandLine';
import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import * as Constants from 'sql/parts/connection/common/constants';
import { IQueryEditorService } from 'sql/parts/query/common/queryEditorService';
import * as platform from 'vs/platform/registry/common/platform';
import { ConnectionProviderProperties, IConnectionProviderRegistry, Extensions as ConnectionProviderExtensions } from 'sql/workbench/parts/connection/common/connectionProviderExtension';
import * as TaskUtilities from 'sql/workbench/common/taskUtilities';
import { IObjectExplorerService } from 'sql/parts/objectExplorer/common/objectExplorerService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';

export class CommandLineService implements ICommandLineProcessing {
private _connectionProfile : ConnectionProfile;
private _showConnectionDialog: boolean;

constructor(
@IConnectionManagementService private _connectionManagementService : IConnectionManagementService,
@ICapabilitiesService private _capabilitiesService : ICapabilitiesService,
@IEnvironmentService private _environmentService : IEnvironmentService,
@IQueryEditorService private _queryEditorService : IQueryEditorService,
@IObjectExplorerService private _objectExplorerService : IObjectExplorerService,
@IEditorService private _editorService : IEditorService,
)
{
let profile = null;
if (this._environmentService && this._environmentService.args.server) {
profile = new ConnectionProfile(_capabilitiesService, null);
// We want connection store to use any matching password it finds
profile.savePassword = true;
Copy link
Member

Choose a reason for hiding this comment

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

is it the case we always want to save the password? there are some security concerns here (particularly on Linux where we don't have an encryption library and depend only on filesystem ACLs)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ConnectionStore only looks for saved credentials that match the profile if savePassword is true. Also, we never pass any password on the command line. So if there's no saved credential that matches the server name/user name/database name combination, this connection attempt will fail, and the user will be shown the connection dialog. At that point they have the option to check/uncheck the Remember password checkbox.

profile.providerName = Constants.mssqlProviderName;
Copy link
Member

Choose a reason for hiding this comment

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

this mechanism will only work for MSSQL connections?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, currently. I assume we'd need to add more switches to support more server types in the future.

profile.serverName = _environmentService.args.server;
profile.databaseName = _environmentService.args.database ? _environmentService.args.database : '';
profile.userName = _environmentService.args.user ? _environmentService.args.user : '';
profile.authenticationType = _environmentService.args.integrated ? 'Integrated' : 'SqlLogin';
profile.connectionName = '';
profile.setOptionValue('applicationName', Constants.applicationName);
profile.setOptionValue('databaseDisplayName', profile.databaseName);
profile.setOptionValue('groupId', profile.groupId);
}
this._connectionProfile = profile;
const registry = platform.Registry.as<IConnectionProviderRegistry>(ConnectionProviderExtensions.ConnectionProviderContributions);
let sqlProvider = registry.getProperties( Constants.mssqlProviderName);
// We can't connect to object explorer until the MSSQL connection provider is registered
if (sqlProvider) {
this.processCommandLine();
} else {
registry.onNewProvider(e => {
if (e.id === Constants.mssqlProviderName)
{
this.processCommandLine();
}
});
}
}
public _serviceBrand: any;
public processCommandLine(): void {
if (!this._connectionProfile && !this._connectionManagementService.hasRegisteredServers()) {
// prompt the user for a new connection on startup if no profiles are registered
this._connectionManagementService.showConnectionDialog();
} else if (this._connectionProfile) {
this._connectionManagementService.connectIfNotConnected(this._connectionProfile, 'connection')
.then(result => TaskUtilities.newQuery(this._connectionProfile,
this._connectionManagementService,
this._queryEditorService,
this._objectExplorerService,
this._editorService))
.catch(() => {});
}
}
}
36 changes: 17 additions & 19 deletions src/sql/parts/connection/common/connectionManagementService.ts
Expand Up @@ -77,7 +77,6 @@ export class ConnectionManagementService extends Disposable implements IConnecti
private _onConnectRequestSent = new Emitter<void>();
private _onConnectionChanged = new Emitter<IConnectionParams>();
private _onLanguageFlavorChanged = new Emitter<sqlops.DidChangeLanguageFlavorParams>();

private _connectionGlobalStatus = new ConnectionGlobalStatus(this._statusBarService);

private _configurationEditService: ConfigurationEditingService;
Expand Down Expand Up @@ -124,16 +123,6 @@ export class ConnectionManagementService extends Disposable implements IConnecti
100 /* High Priority */
));

if (_capabilitiesService && Object.keys(_capabilitiesService.providers).length > 0 && !this.hasRegisteredServers()) {
// prompt the user for a new connection on startup if no profiles are registered
this.showConnectionDialog();
} else if (_capabilitiesService && !this.hasRegisteredServers()) {
_capabilitiesService.onCapabilitiesRegistered(e => {
// prompt the user for a new connection on startup if no profiles are registered
this.showConnectionDialog();
});
}

const registry = platform.Registry.as<IConnectionProviderRegistry>(ConnectionProviderExtensions.ConnectionProviderContributions);

let providerRegistration = (p: { id: string, properties: ConnectionProviderProperties }) => {
Expand Down Expand Up @@ -282,29 +271,30 @@ export class ConnectionManagementService extends Disposable implements IConnecti
* @param options to use after the connection is complete
*/
private tryConnect(connection: IConnectionProfile, owner: IConnectableInput, options?: IConnectionCompletionOptions): Promise<IConnectionResult> {
let self = this;
return new Promise<IConnectionResult>((resolve, reject) => {
// Load the password if it's not already loaded
this._connectionStore.addSavedPassword(connection).then(result => {
self._connectionStore.addSavedPassword(connection).then(result => {
let newConnection = result.profile;
let foundPassword = result.savedCred;

// If there is no password, try to load it from an existing connection
if (!foundPassword && this._connectionStore.isPasswordRequired(newConnection)) {
let existingConnection = this._connectionStatusManager.findConnectionProfile(connection);
if (!foundPassword && self._connectionStore.isPasswordRequired(newConnection)) {
let existingConnection = self._connectionStatusManager.findConnectionProfile(connection);
if (existingConnection && existingConnection.connectionProfile) {
newConnection.password = existingConnection.connectionProfile.password;
foundPassword = true;
}
}
// If the password is required and still not loaded show the dialog
if (!foundPassword && this._connectionStore.isPasswordRequired(newConnection) && !newConnection.password) {
resolve(this.showConnectionDialogOnError(connection, owner, { connected: false, errorMessage: undefined, callStack: undefined, errorCode: undefined }, options));
if (!foundPassword && self._connectionStore.isPasswordRequired(newConnection) && !newConnection.password) {
resolve(self.showConnectionDialogOnError(connection, owner, { connected: false, errorMessage: undefined, callStack: undefined, errorCode: undefined }, options));
} else {
// Try to connect
this.connectWithOptions(newConnection, owner.uri, options, owner).then(connectionResult => {
self.connectWithOptions(newConnection, owner.uri, options, owner).then(connectionResult => {
if (!connectionResult.connected && !connectionResult.errorHandled) {
// If connection fails show the dialog
resolve(this.showConnectionDialogOnError(connection, owner, connectionResult, options));
resolve(self.showConnectionDialogOnError(connection, owner, connectionResult, options));
} else {
//Resolve with the connection result
resolve(connectionResult);
Expand Down Expand Up @@ -390,7 +380,15 @@ export class ConnectionManagementService extends Disposable implements IConnecti
if (this._connectionStatusManager.isConnected(ownerUri)) {
resolve(this._connectionStatusManager.getOriginalOwnerUri(ownerUri));
} else {
this.connect(connection, ownerUri).then(connectionResult => {
const options: IConnectionCompletionOptions = {
// Should saving the connection be a command line switch?
saveTheConnection : true,
showConnectionDialogOnError : true,
showDashboard : purpose === 'dashboard',
params : undefined,
showFirewallRuleOnError : true,
};
this.connect(connection, ownerUri, options).then(connectionResult => {
if (connectionResult && connectionResult.connected) {
resolve(this._connectionStatusManager.getOriginalOwnerUri(ownerUri));
} else {
Expand Down
176 changes: 176 additions & 0 deletions src/sqltest/parts/commandLine/commandLineService.test.ts
@@ -0,0 +1,176 @@
/*---------------------------------------------------------------------------------------------
* 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 * as Constants from 'sql/parts/connection/common/constants';
import * as Utils from 'sql/parts/connection/common/utils';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import * as sqlops from 'sqlops';

import { TPromise } from 'vs/base/common/winjs.base';
import * as assert from 'assert';
import * as TypeMoq from 'typemoq';

import { ConnectionProfile } from 'sql/parts/connection/common/connectionProfile';
import { CommandLineService } from 'sql/parts/commandLine/common/commandLineService';
import { EnvironmentService } from 'vs/platform/environment/node/environmentService';
import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment';
import { CapabilitiesService, ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService';
import { CapabilitiesTestService } from 'sqltest/stubs/capabilitiesTestService';
import { QueryEditorService } from 'sql/parts/query/services/queryEditorService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ObjectExplorerService } from 'sql/parts/objectExplorer/common/objectExplorerService';
import {
IConnectionManagementService, IConnectionDialogService, INewConnectionParams,
ConnectionType, IConnectableInput, IConnectionCompletionOptions, IConnectionCallbacks,
IConnectionParams, IConnectionResult, IServerGroupController, IServerGroupDialogCallbacks,
RunQueryOnConnectionMode
} from 'sql/parts/connection/common/connectionManagement';
import { ConnectionStore } from 'sql/parts/connection/common/connectionStore';
import { TestConnectionManagementService } from 'sqltest/stubs/connectionManagementService.test';


class TestParsedArgs implements ParsedArgs{
[arg: string]: any;
_: string[];
aad?: boolean;
add?: boolean;
database?:string;
debugBrkPluginHost?: string;
debugBrkSearch?: string;
debugId?: string;
debugPluginHost?: string;
debugSearch?: string;
diff?: boolean;
'disable-crash-reporter'?: string;
'disable-extension'?: string | string[];
'disable-extensions'?: boolean;
'disable-restore-windows'?: boolean;
'disable-telemetry'?: boolean;
'disable-updates'?: string;
'driver'?: string;
'enable-proposed-api'?: string | string[];
'export-default-configuration'?: string;
'extensions-dir'?: string;
extensionDevelopmentPath?: string;
extensionTestsPath?: string;
'file-chmod'?: boolean;
'file-write'?: boolean;
'folder-uri'?: string | string[];
goto?: boolean;
help?: boolean;
'install-extension'?: string | string[];
'install-source'?: string;
integrated?: boolean;
'list-extensions'?: boolean;
locale?: string;
log?: string;
logExtensionHostCommunication?: boolean;
'max-memory'?: number;
'new-window'?: boolean;
'open-url'?: boolean;
performance?: boolean;
'prof-append-timers'?: string;
'prof-startup'?: string;
'prof-startup-prefix'?: string;
'reuse-window'?: boolean;
server?: string;
'show-versions'?: boolean;
'skip-add-to-recently-opened'?: boolean;
'skip-getting-started'?: boolean;
'skip-release-notes'?: boolean;
status?: boolean;
'sticky-quickopen'?: boolean;
'uninstall-extension'?: string | string[];
'unity-launch'?: boolean; // Always open a new window, except if opening the first window or opening a file or folder as part of the launch.
'upload-logs'?: string;
user?: string;
'user-data-dir'?: string;
_urls?: string[];
verbose?: boolean;
version?: boolean;
wait?: boolean;
waitMarkerFilePath?: string;
}
suite('commandLineService tests', () => {

let capabilitiesService: CapabilitiesTestService;
let commandLineService : CommandLineService;
let environmentService : TypeMoq.Mock<EnvironmentService>;
let queryEditorService : TypeMoq.Mock<QueryEditorService>;
let editorService:TypeMoq.Mock<IEditorService>;
let objectExplorerService : TypeMoq.Mock<ObjectExplorerService>;
let connectionStore: TypeMoq.Mock<ConnectionStore>;

setup(() => {
capabilitiesService = new CapabilitiesTestService();
connectionStore = TypeMoq.Mock.ofType(ConnectionStore);
});

function getCommandLineService(connectionManagementService : IConnectionManagementService,
environmentService? : IEnvironmentService,
capabilitiesService? : ICapabilitiesService
) : CommandLineService
{
let service= new CommandLineService(
connectionManagementService,
capabilitiesService,
environmentService,
undefined,
undefined,
undefined
);
return service;
}

test('processCommandLine shows connection dialog by default', done => {
const connectionManagementService : TypeMoq.Mock<IConnectionManagementService>
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);

connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable();
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => false);
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.verifiable(TypeMoq.Times.never());
let service = getCommandLineService(connectionManagementService.object);
service.processCommandLine();
connectionManagementService.verifyAll();
done();
});

test('processCommandLine does nothing if registered servers exist and no server name is provided', done => {
const connectionManagementService : TypeMoq.Mock<IConnectionManagementService>
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);

connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never());
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true);
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.verifiable(TypeMoq.Times.never());
let service = getCommandLineService(connectionManagementService.object);
service.processCommandLine();
connectionManagementService.verifyAll();
done();
});

test('processCommandLine opens a new connection if a server name is passed', done => {
const connectionManagementService : TypeMoq.Mock<IConnectionManagementService>
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);

const environmentService : TypeMoq.Mock<IEnvironmentService> = TypeMoq.Mock.ofType<IEnvironmentService>(EnvironmentService);
const args : TestParsedArgs = new TestParsedArgs();
args.server = 'myserver';
args.database = 'mydatabase';
environmentService.setup(e => e.args).returns(() => args).verifiable(TypeMoq.Times.atLeastOnce());
connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never());
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true).verifiable(TypeMoq.Times.atMostOnce());
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.isAny(), 'connection'))
.returns(() => new Promise<string>((resolve, reject) => { reject('unused');}))
.verifiable(TypeMoq.Times.once());
let service = getCommandLineService(connectionManagementService.object, environmentService.object, capabilitiesService);
service.processCommandLine();
environmentService.verifyAll();
connectionManagementService.verifyAll();
done();
});
});
7 changes: 7 additions & 0 deletions src/vs/platform/environment/common/environment.ts
Expand Up @@ -62,6 +62,13 @@ export interface ParsedArgs {
'upload-logs'?: string;
'driver'?: string;
'driver-verbose'?: boolean;
// {{SQL CARBON EDIT}}
aad?: boolean;
database?: string;
integrated?: boolean;
server?: string;
user?: string;
// {{SQL CARBON EDIT}}
}

export const IEnvironmentService = createDecorator<IEnvironmentService>('environmentService');
Expand Down