Updated credentials system (#17888)

* linting

* added flags

* remove testing values

* format

* format doc

* tested in linux

* remove unused interface

* comments

* review comments

* clean imports

* pr comments

* format doc

* changed promise location

* insiders december

* pr comments

* test ado change

* fix test

* comment out code for hygiene

* remove unused imports

* test creds from client only

* remove unused import

* trying enabling keytar

* trying enabling keytar

* disable in correct script

* print statements

* remove print statements

* check mock output

* add linux check

* remove print statements
This commit is contained in:
Aditya Bist
2021-12-17 10:55:42 -08:00
committed by GitHub
parent d8467f2c1a
commit 2a681605fa
10 changed files with 198 additions and 142 deletions

View File

@@ -3,63 +3,13 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { SqlOpsDataClient, ClientOptions, SqlOpsFeature } from 'dataprotocol-client';
import { SqlOpsDataClient, ClientOptions } from 'dataprotocol-client';
import { IConfig } from '@microsoft/ads-service-downloader';
import { ServerOptions, RPCMessageType, ClientCapabilities, ServerCapabilities, TransportKind } from 'vscode-languageclient';
import { Disposable } from 'vscode';
import * as UUID from 'vscode-languageclient/lib/utils/uuid';
import * as azdata from 'azdata';
import * as Contracts from './contracts';
import { ServerOptions, TransportKind } from 'vscode-languageclient';
import * as Constants from './constants';
import * as Utils from '../utils';
class CredentialsFeature extends SqlOpsFeature<any> {
private static readonly messagesTypes: RPCMessageType[] = [
Contracts.DeleteCredentialRequest.type,
Contracts.SaveCredentialRequest.type,
Contracts.ReadCredentialRequest.type
];
constructor(client: SqlOpsDataClient) {
super(client, CredentialsFeature.messagesTypes);
}
fillClientCapabilities(capabilities: ClientCapabilities): void {
Utils.ensure(Utils.ensure(capabilities, 'credentials')!, 'credentials')!.dynamicRegistration = true;
}
initialize(capabilities: ServerCapabilities): void {
this.register(this.messages, {
id: UUID.generateUuid(),
registerOptions: undefined
});
}
protected registerProvider(options: any): Disposable {
const client = this._client;
let readCredential = (credentialId: string): Thenable<azdata.Credential> => {
return client.sendRequest(Contracts.ReadCredentialRequest.type, { credentialId, password: undefined });
};
let saveCredential = (credentialId: string, password: string): Thenable<boolean> => {
return client.sendRequest(Contracts.SaveCredentialRequest.type, { credentialId, password });
};
let deleteCredential = (credentialId: string): Thenable<boolean> => {
return client.sendRequest(Contracts.DeleteCredentialRequest.type, { credentialId, password: undefined });
};
return azdata.credentials.registerProvider({
deleteCredential,
readCredential,
saveCredential,
handle: 0
});
}
}
import { SqlCredentialService } from './sqlCredentialService';
import { AppContext } from '../appContext';
/**
* Implements a credential storage for Windows, Mac (darwin), or Linux.
@@ -69,18 +19,24 @@ class CredentialsFeature extends SqlOpsFeature<any> {
export class CredentialStore {
private _client: SqlOpsDataClient;
private _config: IConfig;
private _logPath: string;
constructor(private logPath: string, baseConfig: IConfig) {
constructor(
private context: AppContext,
baseConfig: IConfig
) {
if (baseConfig) {
this._config = JSON.parse(JSON.stringify(baseConfig));
this._config.executableFiles = ['MicrosoftSqlToolsCredentials.exe', 'MicrosoftSqlToolsCredentials'];
}
this.context = context;
this._logPath = this.context.extensionContext.logPath;
}
public async start(): Promise<void> {
let clientOptions: ClientOptions = {
providerId: Constants.providerId,
features: [CredentialsFeature]
features: [SqlCredentialService.asFeature(this.context)]
};
const serverPath = await Utils.getOrDownloadServer(this._config);
const serverOptions = this.generateServerOptions(serverPath);
@@ -95,7 +51,7 @@ export class CredentialStore {
}
private generateServerOptions(executablePath: string): ServerOptions {
let launchArgs = Utils.getCommonLaunchArgsAndCleanupOldLogFiles(this.logPath, 'credentialstore.log', executablePath);
let launchArgs = Utils.getCommonLaunchArgsAndCleanupOldLogFiles(this._logPath, 'credentialstore.log', executablePath);
return { command: executablePath, args: launchArgs, transport: TransportKind.stdio };
}
}

View File

@@ -0,0 +1,91 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AppContext } from '../appContext';
import { SqlOpsDataClient, ISqlOpsFeature, SqlOpsFeature } from 'dataprotocol-client';
import * as Utils from '../utils';
import { ClientCapabilities, RPCMessageType, ServerCapabilities } from 'vscode-languageclient';
import * as Contracts from './contracts';
import { Disposable, SecretStorage } from 'vscode';
import * as azdata from 'azdata';
import * as UUID from 'vscode-languageclient/lib/utils/uuid';
export class SqlCredentialService extends SqlOpsFeature<any> {
private static readonly messagesTypes: RPCMessageType[] = [
Contracts.DeleteCredentialRequest.type,
Contracts.SaveCredentialRequest.type,
Contracts.ReadCredentialRequest.type
];
public static asFeature(context: AppContext): ISqlOpsFeature {
return class extends SqlCredentialService {
private _secretStorage: SecretStorage;
constructor(client: SqlOpsDataClient) {
super(context, client);
this._secretStorage = context.extensionContext.secrets;
}
override fillClientCapabilities(capabilities: ClientCapabilities): void {
Utils.ensure(Utils.ensure(capabilities, 'credentials')!, 'credentials')!.dynamicRegistration = true;
}
override async initialize(capabilities: ServerCapabilities): Promise<void> {
this.register(this.messages, {
id: UUID.generateUuid(),
registerOptions: undefined
});
}
protected override registerProvider(options: any): Disposable {
let readCredential = async (credentialId: string): Promise<azdata.Credential> => {
return this._client.sendRequest(Contracts.ReadCredentialRequest.type, { credentialId, password: undefined });
};
let saveCredential = async (credentialId: string, password: string): Promise<boolean> => {
if (Utils.isLinux) {
/**
* This is only done for linux because this is going to be
* the default credential system for linux in the next release
*/
await this._secretStorage.store(credentialId, password);
}
return this._client.sendRequest(Contracts.SaveCredentialRequest.type, { credentialId, password });
};
let deleteCredential = async (credentialId: string): Promise<boolean> => {
if (Utils.isLinux) {
try {
await this._secretStorage.delete(credentialId);
} catch (e) {
console.log('credential does not exist in native secret store');
}
}
return this._client.sendRequest(Contracts.DeleteCredentialRequest.type, { credentialId, password: undefined });
};
return azdata.credentials.registerProvider({
deleteCredential,
readCredential,
saveCredential,
handle: 0
});
}
};
}
fillClientCapabilities(capabilities: ClientCapabilities): void { }
initialize(capabilities: ServerCapabilities): void { }
protected registerProvider(options: any): Disposable { return undefined; }
private constructor(context: AppContext, protected readonly client: SqlOpsDataClient) {
super(client, SqlCredentialService.messagesTypes);
}
}

View File

@@ -25,6 +25,7 @@ import { LanguageExtensionService } from './languageExtension/languageExtensionS
import { SqlAssessmentService } from './sqlAssessment/sqlAssessmentService';
import { NotebookConvertService } from './notebookConvert/notebookConvertService';
import { SqlMigrationService } from './sqlMigration/sqlMigrationService';
import { SqlCredentialService } from './credentialstore/sqlCredentialService';
const localize = nls.loadMessageBundle();
const outputChannel = vscode.window.createOutputChannel(Constants.serviceName);
@@ -86,7 +87,7 @@ export class SqlToolsServer {
}
private activateFeatures(context: AppContext): Promise<void> {
const credsStore = new CredentialStore(context.extensionContext.logPath, this.config);
const credsStore = new CredentialStore(context, this.config);
const resourceProvider = new AzureResourceProvider(context.extensionContext.logPath, this.config);
this.disposables.push(credsStore);
this.disposables.push(resourceProvider);
@@ -163,6 +164,7 @@ function getClientOptions(context: AppContext): ClientOptions {
NotebookConvertService.asFeature(context),
ProfilerFeature,
SqlMigrationService.asFeature(context),
SqlCredentialService.asFeature(context),
TableDesignerFeature
],
outputChannel: new CustomOutputChannel()

View File

@@ -21,6 +21,12 @@ const configLogFilesRemovalLimit = 'logFilesRemovalLimit';
const extensionConfigSectionName = 'mssql';
const configLogDebugInfo = 'logDebugInfo';
/**
*
* @returns Whether the current OS is linux or not
*/
export const isLinux = os.platform() === 'linux';
// The function is a duplicate of \src\paths.js. IT would be better to import path.js but it doesn't
// work for now because the extension is running in different process.
export function getAppDataPath() {
@@ -64,8 +70,7 @@ export function getConfigLogFilesRemovalLimit(): number {
let config = getConfiguration();
if (config) {
return Number((config[configLogFilesRemovalLimit]).toFixed(0));
}
else {
} else {
return undefined;
}
}
@@ -74,8 +79,7 @@ export function getConfigLogRetentionSeconds(): number {
let config = getConfiguration();
if (config) {
return Number((config[configLogRetentionMinutes] * 60).toFixed(0));
}
else {
} else {
return undefined;
}
}
@@ -84,8 +88,7 @@ export function getConfigTracingLevel(): string {
let config = getConfiguration();
if (config) {
return config[configTracingLevel];
}
else {
} else {
return undefined;
}
}