diff --git a/resources/xlf/en/sql.xlf b/resources/xlf/en/sql.xlf index fc5b612de4..f653f3c951 100644 --- a/resources/xlf/en/sql.xlf +++ b/resources/xlf/en/sql.xlf @@ -5244,6 +5244,17 @@ Error: {1} If you have previously connected you may need to re-run kinit. + + Run Kinit + + + Enable Trust server certificate + + + Encryption was enabled on this connection, review your SSL and certificate configuration for the target SQL Server, or enable 'Trust server certificate' in the connection dialog. + + 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? + diff --git a/src/sql/platform/connection/common/constants.ts b/src/sql/platform/connection/common/constants.ts index 3d08bbc393..9251bfffb2 100644 --- a/src/sql/platform/connection/common/constants.ts +++ b/src/sql/platform/connection/common/constants.ts @@ -24,6 +24,9 @@ export const passwordChars = '***************'; /* default authentication type setting name*/ export const defaultAuthenticationType = 'defaultAuthenticationType'; +/* Connection Properties */ +export const trustServerCertificate = 'trustServerCertificate'; + /** * Well-known Authentication types commonly supported by connection providers. */ diff --git a/src/sql/platform/errorMessage/common/errorMessageService.ts b/src/sql/platform/errorMessage/common/errorMessageService.ts index 4fdaf06e75..355c03cfcd 100644 --- a/src/sql/platform/errorMessage/common/errorMessageService.ts +++ b/src/sql/platform/errorMessage/common/errorMessageService.ts @@ -10,5 +10,15 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' export const IErrorMessageService = createDecorator('errorMessageService'); export interface IErrorMessageService { _serviceBrand: undefined; - showDialog(severity: Severity, headerTitle: string, message: string, messageDetails?: string, actions?: IAction[]): void; + /** + * Shows error dialog with given parameters + * @param severity Severity of the error + * @param headerTitle Title to show on Error modal dialog + * @param message Message containng error message + * @param messageDetails Message details containing stacktrace along with error message + * @param actions Custom actions to display on the error message dialog + * @param instructionText Spcial instructions to display to user when displaying error message + * @param readMoreLink External link to read more about the instructions. + */ + showDialog(severity: Severity, headerTitle: string, message: string, messageDetails?: string, actions?: IAction[], instructionText?: string, readMoreLink?: string): void; } diff --git a/src/sql/workbench/services/connection/browser/connectionDialogService.ts b/src/sql/workbench/services/connection/browser/connectionDialogService.ts index 693388396f..94437cdb26 100644 --- a/src/sql/workbench/services/connection/browser/connectionDialogService.ts +++ b/src/sql/workbench/services/connection/browser/connectionDialogService.ts @@ -270,7 +270,7 @@ export class ConnectionDialogService implements IConnectionDialogService { this._logService.debug(`ConnectionDialogService: Error handled and connection reset - Error: ${connectionResult.errorMessage}`); } else { this._connectionDialog.resetConnection(); - this.showErrorDialog(Severity.Error, this._connectionErrorTitle, connectionResult.errorMessage, connectionResult.callStack); + this.showErrorDialog(Severity.Error, this._connectionErrorTitle, connectionResult.errorMessage, connectionResult.callStack, connectionResult.errorCode); this._logService.debug(`ConnectionDialogService: Connection error: ${connectionResult.errorMessage}`); } } catch (err) { @@ -456,7 +456,7 @@ export class ConnectionDialogService implements IConnectionDialogService { await this.showDialogWithModel(); if (connectionResult && connectionResult.errorMessage) { - this.showErrorDialog(Severity.Error, this._connectionErrorTitle, connectionResult.errorMessage, connectionResult.callStack); + this.showErrorDialog(Severity.Error, this._connectionErrorTitle, connectionResult.errorMessage, connectionResult.callStack, connectionResult.errorCode); } } @@ -488,7 +488,7 @@ export class ConnectionDialogService implements IConnectionDialogService { recentConnections.forEach(conn => conn.dispose()); } - private showErrorDialog(severity: Severity, headerTitle: string, message: string, messageDetails?: string): void { + private showErrorDialog(severity: Severity, headerTitle: string, message: string, messageDetails?: string, errorCode?: number): void { // Kerberos errors are currently very hard to understand, so adding handling of these to solve the common scenario // note that ideally we would have an extensible service to handle errors by error code and provider, but for now // this solves the most common "hard error" that we've noticed @@ -502,7 +502,7 @@ export class ConnectionDialogService implements IConnectionDialogService { localize('kerberosHelpLink', "Help configuring Kerberos is available at {0}", helpLink), localize('kerberosKinit', "If you have previously connected you may need to re-run kinit.") ].join('\r\n'); - actions.push(new Action('Kinit', 'Run kinit', null, true, async () => { + actions.push(new Action('Kinit', localize('runKinit', "Run Kinit"), undefined, true, async () => { this._connectionDialog.close(); await this._clipboardService.writeText('kinit\r'); await this._commandService.executeCommand('workbench.action.terminal.focus'); @@ -512,9 +512,25 @@ export class ConnectionDialogService implements IConnectionDialogService { }, 10); return; })); - } + this._logService.error(message); - this._errorMessageService.showDialog(severity, headerTitle, message, messageDetails, actions); + + // Set instructionText for MSSQL Provider Encryption error code -2146893019 thrown by SqlClient when certificate validation fails. + if (errorCode === -2146893019) { + let enableTrustServerCert = localize('enableTrustServerCertificate', "Enable Trust server certificate"); + let instructionText = localize('trustServerCertInstructionText', `Encryption was enabled on this connection, review your SSL and certificate configuration for the target SQL Server, or enable 'Trust server certificate' in the connection dialog. + + 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? `); + let readMoreLink = "https://learn.microsoft.com/sql/database-engine/configure-windows/enable-encrypted-connections-to-the-database-engine" + actions.push(new Action('trustServerCert', enableTrustServerCert, undefined, true, async () => { + this._model.options[Constants.trustServerCertificate] = true; + await this.handleOnConnect(this._connectionDialog.newConnectionParams, this._model as IConnectionProfile); + return; + })); + this._errorMessageService.showDialog(severity, headerTitle, message, messageDetails, actions, instructionText, readMoreLink); + } else { + this._errorMessageService.showDialog(severity, headerTitle, message, messageDetails, actions); + } } } diff --git a/src/sql/workbench/services/errorMessage/browser/errorMessageDialog.ts b/src/sql/workbench/services/errorMessage/browser/errorMessageDialog.ts index caafb51d3e..36b4722ecc 100644 --- a/src/sql/workbench/services/errorMessage/browser/errorMessageDialog.ts +++ b/src/sql/workbench/services/errorMessage/browser/errorMessageDialog.ts @@ -24,6 +24,8 @@ import { attachModalDialogStyler } from 'sql/workbench/common/styler'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { attachButtonStyler } from 'vs/platform/theme/common/styler'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfiguration'; +import { Link } from 'vs/platform/opener/browser/link'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; const maxActions = 1; @@ -36,9 +38,12 @@ export class ErrorMessageDialog extends Modal { private _actions: IAction[] = []; private _severity?: Severity; private _message?: string; + private _instructionText?: string; + private _readMoreLink?: string; private _messageDetails?: string; private _okLabel: string; private _closeLabel: string; + private _readMoreLabel: string; private _onOk = new Emitter(); public onOk: Event = this._onOk.event; @@ -50,11 +55,13 @@ export class ErrorMessageDialog extends Modal { @IAdsTelemetryService telemetryService: IAdsTelemetryService, @IContextKeyService contextKeyService: IContextKeyService, @ILogService logService: ILogService, - @ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService + @ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService, + @IOpenerService private readonly _openerService: IOpenerService ) { super('', TelemetryKeys.ModalDialogName.ErrorMessage, telemetryService, layoutService, clipboardService, themeService, logService, textResourcePropertiesService, contextKeyService, { dialogStyle: 'normal', hasTitleIcon: true }); this._okLabel = localize('errorMessageDialog.ok', "OK"); this._closeLabel = localize('errorMessageDialog.close', "Close"); + this._readMoreLabel = localize('errorMessageDialog.readMore', "Read More"); } protected renderBody(container: HTMLElement) { @@ -88,7 +95,7 @@ export class ErrorMessageDialog extends Modal { } private createStandardButton(label: string, onSelect: () => void): Button { - let button = this.addFooterButton(label, onSelect, 'right', true); + let button = this.addFooterButton(label, onSelect, 'right', false); this._register(attachButtonStyler(button, this._themeService)); return button; } @@ -109,6 +116,17 @@ export class ErrorMessageDialog extends Modal { protected updateDialogBody(): void { DOM.clearNode(this._body!); DOM.append(this._body!, DOM.$('div.error-message')).innerText = this._message!; + if (this._instructionText) { + let childElement = DOM.$('div.error-instruction-text'); + childElement.innerText = this._instructionText!; + if (this._readMoreLink) { + new Link(childElement, { + label: this._readMoreLabel, + href: this._readMoreLink + }, undefined, this._openerService); + } + DOM.append(this._body!, childElement); + } } protected getBody(): HTMLElement { @@ -148,9 +166,11 @@ export class ErrorMessageDialog extends Modal { this.hide(hideReason); } - public open(severity: Severity, headerTitle: string, message: string, messageDetails?: string, actions?: IAction[]) { + public open(severity: Severity, headerTitle: string, message: string, messageDetails?: string, actions?: IAction[], instructionText?: string, readMoreLink?: string): void { this._severity = severity; this._message = message; + this._instructionText = instructionText; + this._readMoreLink = readMoreLink; this.title = headerTitle; this._messageDetails = messageDetails; if (this._messageDetails) { @@ -162,21 +182,31 @@ export class ErrorMessageDialog extends Modal { this._bodyContainer.setAttribute('aria-description', this._message); } this.resetActions(); - if (actions && actions.length > 0) { + if (actions?.length > 0) { for (let i = 0; i < maxActions && i < actions.length; i++) { this._actions.push(actions[i]); let button = this._actionButtons[i]; button.label = actions[i].label; button.element.style.visibility = 'visible'; } - this._okButton!.label = this._closeLabel; + //Remove and add button again to update style. + this.removeFooterButton(this._okLabel); + this.removeFooterButton(this._closeLabel); + this._okButton = this.addFooterButton(this._closeLabel, () => this.ok(), undefined, true); } else { - this._okButton!.label = this._okLabel; + //Remove and add button again to update style + this.removeFooterButton(this._okLabel); + this.removeFooterButton(this._closeLabel); + this._okButton = this.addFooterButton(this._okLabel, () => this.ok()); } this.updateIconTitle(); this.updateDialogBody(); this.show(); - this._okButton!.focus(); + if (actions?.length > 0) { + this._actionButtons[0].focus(); + } else { + this._okButton!.focus(); + } } private resetActions(): void { diff --git a/src/sql/workbench/services/errorMessage/browser/errorMessageService.ts b/src/sql/workbench/services/errorMessage/browser/errorMessageService.ts index 25af7b4103..01a789143d 100644 --- a/src/sql/workbench/services/errorMessage/browser/errorMessageService.ts +++ b/src/sql/workbench/services/errorMessage/browser/errorMessageService.ts @@ -24,11 +24,11 @@ export class ErrorMessageService implements IErrorMessageService { @IInstantiationService private _instantiationService: IInstantiationService ) { } - public showDialog(severity: Severity, headerTitle: string, message: string, messageDetails?: string, actions?: IAction[]): void { - this.doShowDialog(severity, headerTitle, message, messageDetails, actions); + public showDialog(severity: Severity, headerTitle: string, message: string, messageDetails?: string, actions?: IAction[], instructionText?: string, readMoreLink?: string): void { + this.doShowDialog(severity, headerTitle, message, messageDetails, actions, instructionText, readMoreLink); } - private doShowDialog(severity: Severity, headerTitle: string, message: string, messageDetails?: string, actions?: IAction[]): void { + private doShowDialog(severity: Severity, headerTitle: string, message: string, messageDetails?: string, actions?: IAction[], instructionText?: string, readMoreLink?: string): void { if (!this._errorDialog) { this._errorDialog = this._instantiationService.createInstance(ErrorMessageDialog); this._errorDialog.onOk(() => this.handleOnOk()); @@ -36,7 +36,7 @@ export class ErrorMessageService implements IErrorMessageService { } let title = headerTitle ? headerTitle : this.getDefaultTitle(severity); - return this._errorDialog.open(severity, title, message, messageDetails, actions); + return this._errorDialog.open(severity, title, message, messageDetails, actions, instructionText, readMoreLink); } private getDefaultTitle(severity: Severity) { diff --git a/src/sql/workbench/services/errorMessage/browser/media/errorMessageDialog.css b/src/sql/workbench/services/errorMessage/browser/media/errorMessageDialog.css index ed4d08cab2..a1121791e1 100644 --- a/src/sql/workbench/services/errorMessage/browser/media/errorMessageDialog.css +++ b/src/sql/workbench/services/errorMessage/browser/media/errorMessageDialog.css @@ -6,7 +6,7 @@ .error-dialog { padding: 15px; overflow: auto; - height: 200px; + height: 210px; } .error-dialog .codicon.error, .error-dialog .codicon.warning , .error-dialog .codicon.info { @@ -22,6 +22,11 @@ user-select: text; } +.error-dialog .error-instruction-text{ + margin-top: 40px; + font-weight: 700; +} + .modal .footer-button a.monaco-button.monaco-text-button.codicon.scriptToClipboard { width: 120px; }