From ef240a9a63c3b0172b330f5ca6eb5b2ca56fc28e Mon Sep 17 00:00:00 2001 From: Cheena Malhotra <13396919+cheenamalhotra@users.noreply.github.com> Date: Tue, 24 Jan 2023 16:20:35 -0800 Subject: [PATCH] [Azure Arc] Update MIAA model to include encryption properties (#21702) --- extensions/arc/src/constants.ts | 4 + extensions/arc/src/localizedConstants.ts | 9 ++ extensions/arc/src/models/miaaModel.ts | 8 +- extensions/arc/src/typings/arc.d.ts | 4 +- .../arc/src/ui/dialogs/connectSqlDialog.ts | 83 +++++++++++++++++-- .../arc/src/ui/tree/controllerTreeNode.ts | 11 ++- 6 files changed, 107 insertions(+), 12 deletions(-) diff --git a/extensions/arc/src/constants.ts b/extensions/arc/src/constants.ts index 8a5692e5d1..d7b4b4f845 100644 --- a/extensions/arc/src/constants.ts +++ b/extensions/arc/src/constants.ts @@ -190,3 +190,7 @@ export namespace cssStyles { } export const iconSize = '20px'; + +export const encryptOption = 'encrypt'; +export const trustServerCertificateOption = 'trustServerCertificate'; +export const encryptReadMoreLink = 'https://learn.microsoft.com/sql/database-engine/configure-windows/enable-encrypted-connections-to-the-database-engine'; diff --git a/extensions/arc/src/localizedConstants.ts b/extensions/arc/src/localizedConstants.ts index 00e90a050e..21bfc0b777 100644 --- a/extensions/arc/src/localizedConstants.ts +++ b/extensions/arc/src/localizedConstants.ts @@ -142,12 +142,21 @@ export const controllerPassword = localize('arc.controllerPassword', "Controller export const username = localize('arc.username', "Username"); export const password = localize('arc.password', "Password"); export const rememberPassword = localize('arc.rememberPassword', "Remember Password"); +export const encrypt = localize('arc.encrypt', "Encrypt"); +export const encryptDescription = localize('arc.encryptDescription', "When true, SQL Server uses SSL encryption for all data sent between the client and server if the server has a certificate installed."); +export const trustServerCertificate = localize('arc.trustServerCertificate', "Trust Server Certificate"); +export const trustServerCertDescription = localize('arc.trustServerCertDescription', "When true (and encrypt=true), SQL Server uses SSL encryption for all data sent between the client and server without validating the server certificate."); +export const enableTrustServerCert = localize('arc.enableTrustServerCert', "Enable Trust Server Certificate"); +export const msgPromptSSLCertificateValidationFailed = localize('arc.msgPromptSSLCertificateValidationFailed', 'Encryption was enabled on this connection, review your SSL and certificate configuration for the target SQL Server, or set \'Trust server certificate\' to \'true\' in the settings file. Note: A self-signed certificate offers only limited protection and is not a recommended practice for production environments. Do you want to enable \'Trust server certificate\' on this connection and retry?'); export const connect = localize('arc.connect', "Connect"); +export const readMore = localize('arc.readMore', "Read more"); export const cancel = localize('arc.cancel', "Cancel"); export const apply = localize('arc.apply', "Apply"); export const ok = localize('arc.ok', "Ok"); export const on = localize('arc.on', "On"); export const off = localize('arc.off', "Off"); +export const booleantrue = localize('arc.booleantrue', "True"); +export const booleanfalse = localize('arc.booleanfalse', "False"); export const notConfigured = localize('arc.notConfigured', "Not Configured"); // Database States - see https://docs.microsoft.com/sql/relational-databases/databases/database-states diff --git a/extensions/arc/src/models/miaaModel.ts b/extensions/arc/src/models/miaaModel.ts index fafdcbbb6d..2cf25771d2 100644 --- a/extensions/arc/src/models/miaaModel.ts +++ b/extensions/arc/src/models/miaaModel.ts @@ -219,7 +219,10 @@ export class MiaaModel extends ResourceModel { saveProfile: true, id: '', groupId: undefined, - options: {} + options: { + encrypt: this._miaaInfo.encrypt || true, + trustServerCertificate: this._miaaInfo.trustServerCertificate || false + } }; } @@ -240,6 +243,8 @@ export class MiaaModel extends ResourceModel { this._activeConnectionId = connectionProfile.id; this.info.connectionId = connectionProfile.id; this._miaaInfo.userName = connectionProfile.userName; + this._miaaInfo.encrypt = connectionProfile.options.encrypt; + this._miaaInfo.trustServerCertificate = connectionProfile.options.trustServerCertificate; await this._treeDataProvider.saveControllers(); } @@ -270,6 +275,5 @@ export class MiaaModel extends ResourceModel { this._databaseTimeWindow.set(dbName, ['', '']); } } - } } diff --git a/extensions/arc/src/typings/arc.d.ts b/extensions/arc/src/typings/arc.d.ts index a5d267ef44..70bcf51164 100644 --- a/extensions/arc/src/typings/arc.d.ts +++ b/extensions/arc/src/typings/arc.d.ts @@ -20,7 +20,9 @@ declare module 'arc' { } export type MiaaResourceInfo = ResourceInfo & { - userName?: string + userName?: string, + encrypt?: string, + trustServerCertificate?: boolean }; export type PGResourceInfo = ResourceInfo & { diff --git a/extensions/arc/src/ui/dialogs/connectSqlDialog.ts b/extensions/arc/src/ui/dialogs/connectSqlDialog.ts index 8f5927424b..935e589f5e 100644 --- a/extensions/arc/src/ui/dialogs/connectSqlDialog.ts +++ b/extensions/arc/src/ui/dialogs/connectSqlDialog.ts @@ -8,11 +8,15 @@ import * as vscode from 'vscode'; import { Deferred } from '../../common/promise'; import * as loc from '../../localizedConstants'; import { createCredentialId } from '../../common/utils'; -import { credentialNamespace } from '../../constants'; +import * as constants from '../../constants'; import { InitializingComponent } from '../components/initializingComponent'; import { ResourceModel } from '../../models/resourceModel'; import { ControllerModel } from '../../models/controllerModel'; +export interface IReconnectAction { + (profile: azdata.IConnectionProfile): Promise; +} + export abstract class ConnectToSqlDialog extends InitializingComponent { protected modelBuilder!: azdata.ModelBuilder; @@ -20,6 +24,9 @@ export abstract class ConnectToSqlDialog extends InitializingComponent { protected usernameInputBox!: azdata.InputBoxComponent; protected passwordInputBox!: azdata.InputBoxComponent; protected rememberPwCheckBox!: azdata.CheckBoxComponent; + protected encryptSelectBox!: azdata.DropDownComponent; + protected trustServerCertificateSelectBox!: azdata.DropDownComponent; + private options: { [name: string]: any } = {}; protected _completionPromise = new Deferred(); @@ -29,7 +36,11 @@ export abstract class ConnectToSqlDialog extends InitializingComponent { } public showDialog(dialogTitle: string, connectionProfile?: azdata.IConnectionProfile): azdata.window.Dialog { - const dialog = azdata.window.createModelViewDialog(dialogTitle); + const dialog = azdata.window.createModelViewDialog(dialogTitle, undefined, 'narrow'); + const trueCategory: azdata.CategoryValue = { displayName: loc.booleantrue, name: 'true' } + const falseCategory: azdata.CategoryValue = { displayName: loc.booleanfalse, name: 'false' } + const booleanCategoryValues: azdata.CategoryValue[] = [trueCategory, falseCategory]; + dialog.cancelButton.onClick(() => this.handleCancel()); dialog.registerContent(async view => { this.modelBuilder = view.modelBuilder; @@ -47,13 +58,22 @@ export abstract class ConnectToSqlDialog extends InitializingComponent { .withProps({ inputType: 'password', value: connectionProfile?.password - }) - .component(); + }).component(); this.rememberPwCheckBox = this.modelBuilder.checkBox() .withProps({ label: loc.rememberPassword, checked: connectionProfile?.savePassword }).component(); + this.encryptSelectBox = this.modelBuilder.dropDown() + .withProps({ + values: booleanCategoryValues, + value: connectionProfile?.options[constants.encryptOption] ? trueCategory : falseCategory + }).component(); + this.trustServerCertificateSelectBox = this.modelBuilder.dropDown() + .withProps({ + values: booleanCategoryValues, + value: connectionProfile?.options[constants.trustServerCertificateOption] ? trueCategory : falseCategory + }).component(); let formModel = this.modelBuilder.formContainer() .withFormItems([{ @@ -73,6 +93,18 @@ export abstract class ConnectToSqlDialog extends InitializingComponent { }, { component: this.rememberPwCheckBox, title: '' + }, { + component: this.encryptSelectBox, + title: loc.encrypt, + layout: { + info: loc.encryptDescription, + } + }, { + component: this.trustServerCertificateSelectBox, + title: loc.trustServerCertificate, + layout: { + info: loc.trustServerCertDescription, + } } ], title: '' @@ -94,6 +126,10 @@ export abstract class ConnectToSqlDialog extends InitializingComponent { if (!this.serverNameInputBox.value || !this.usernameInputBox.value || !this.passwordInputBox.value) { return false; } + + this.options.encrypt = this.encryptSelectBox.value; + this.options.trustServerCertificate = this.trustServerCertificateSelectBox.value; + const connectionProfile: azdata.IConnectionProfile = { serverName: this.serverNameInputBox.value, databaseName: '', @@ -109,10 +145,15 @@ export abstract class ConnectToSqlDialog extends InitializingComponent { groupId: undefined, options: this.options }; + + return await this.connect(connectionProfile); + } + + private async connect(connectionProfile: azdata.IConnectionProfile): Promise { const result = await azdata.connection.connect(connectionProfile, false, false); if (result.connected) { connectionProfile.id = result.connectionId!; - const credentialProvider = await azdata.credentials.getProvider(credentialNamespace); + const credentialProvider = await azdata.credentials.getProvider(constants.credentialNamespace); if (connectionProfile.savePassword) { await credentialProvider.saveCredential(createCredentialId(this._controllerModel.info.id, this._model.info.resourceType, this._model.info.name), connectionProfile.password); } else { @@ -123,10 +164,40 @@ export abstract class ConnectToSqlDialog extends InitializingComponent { } else { vscode.window.showErrorMessage(this.connectionFailedMessage(result.errorMessage)); - return false; + // Show error with instructions for MSSQL Provider Encryption error code -2146893019 thrown by SqlClient when certificate validation fails. + if (result.errorCode === -2146893019) { + return this.showInstructionTextAsWarning(connectionProfile, async updatedConnection => { + return await this.connect(updatedConnection); + }); + } else { + return false; + } } } + private async showInstructionTextAsWarning(profile: azdata.IConnectionProfile, reconnectAction: IReconnectAction): Promise { + while (true) { + const selection = await vscode.window.showWarningMessage( + loc.msgPromptSSLCertificateValidationFailed, + { modal: false }, + ...[ + loc.enableTrustServerCert, + loc.readMore, + loc.cancel + ]); + if (selection === loc.enableTrustServerCert) { + profile.options.encrypt = true; + profile.options.trustServerCertificate = true; + return await reconnectAction(profile); + } else if (selection === loc.readMore) { + vscode.env.openExternal(vscode.Uri.parse(constants.encryptReadMoreLink)); + // Show the dialog again so the user can still pick yes or no after they've read the docs + continue; + } else { + return false; + } + } + } protected abstract get providerName(): string; protected abstract connectionFailedMessage(error: any): string; diff --git a/extensions/arc/src/ui/tree/controllerTreeNode.ts b/extensions/arc/src/ui/tree/controllerTreeNode.ts index e3f8f2dee8..30a1020b9b 100644 --- a/extensions/arc/src/ui/tree/controllerTreeNode.ts +++ b/extensions/arc/src/ui/tree/controllerTreeNode.ts @@ -110,10 +110,15 @@ export class ControllerTreeNode extends TreeNode { node = new PostgresTreeNode(postgresModel, this.model); break; case ResourceType.sqlManagedInstances: - // Fill in the username too if we already have it - (resourceInfo as MiaaResourceInfo).userName = (this.model.info.resources.find(info => + // Fill in the username and connection properties too if we already have them + let miaaResourceInfo = this.model.info.resources.find(info => info.name === resourceInfo.name && - info.resourceType === resourceInfo.resourceType) as MiaaResourceInfo)?.userName; + info.resourceType === resourceInfo.resourceType) as MiaaResourceInfo; + if (miaaResourceInfo) { + (resourceInfo as MiaaResourceInfo).userName = miaaResourceInfo.userName; + (resourceInfo as MiaaResourceInfo).encrypt = miaaResourceInfo.encrypt; + (resourceInfo as MiaaResourceInfo).trustServerCertificate = miaaResourceInfo.trustServerCertificate; + } const miaaModel = new MiaaModel(this.model, resourceInfo, registration, this._treeDataProvider); node = new MiaaTreeNode(miaaModel, this.model); break;