Migrate cert validation error handling to mssql extension (#21829)

This commit is contained in:
Cheena Malhotra
2023-02-07 09:21:35 -08:00
committed by GitHub
parent e1b35d266a
commit 66410edf02
29 changed files with 352 additions and 92 deletions

View File

@@ -27,8 +27,7 @@ import * as types from 'vs/base/common/types';
import { trim } from 'vs/base/common/strings';
import { localize } from 'vs/nls';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
import { TelemetryView } from 'sql/platform/telemetry/common/telemetryKeys';
import { CmsConnectionController } from 'sql/workbench/services/connection/browser/cmsConnectionController';
import { entries } from 'sql/base/common/collections';
import { onUnexpectedError } from 'vs/base/common/errors';
@@ -91,7 +90,6 @@ export class ConnectionDialogService implements IConnectionDialogService {
@IClipboardService private _clipboardService: IClipboardService,
@ICommandService private _commandService: ICommandService,
@ILogService private _logService: ILogService,
@IAdsTelemetryService private _telemetryService: IAdsTelemetryService
) {
this.initializeConnectionProviders();
}
@@ -285,7 +283,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, connectionResult.errorCode);
this.showErrorDialog(Severity.Error, this._connectionErrorTitle, connectionResult.errorMessage, connectionResult.messageDetails);
this._logService.debug(`ConnectionDialogService: Connection error: ${connectionResult.errorMessage}`);
}
} catch (err) {
@@ -471,7 +469,7 @@ export class ConnectionDialogService implements IConnectionDialogService {
await this.showDialogWithModel();
if (connectionResult && connectionResult.errorMessage) {
this.showErrorDialog(Severity.Error, this._connectionErrorTitle, connectionResult.errorMessage, connectionResult.callStack, connectionResult.errorCode);
this.showErrorDialog(Severity.Error, this._connectionErrorTitle, connectionResult.errorMessage, connectionResult.messageDetails);
}
}
@@ -503,8 +501,7 @@ export class ConnectionDialogService implements IConnectionDialogService {
recentConnections.forEach(conn => conn.dispose());
}
private showErrorDialog(severity: Severity, headerTitle: string, message: string, messageDetails?: string, errorCode?: number): void {
private showErrorDialog(severity: Severity, headerTitle: string, message: string, messageDetails?: string): 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
@@ -532,22 +529,6 @@ export class ConnectionDialogService implements IConnectionDialogService {
this._logService.error(message);
// 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._telemetryService.sendActionEvent(TelemetryKeys.TelemetryView.ConnectionDialog, TelemetryKeys.TelemetryAction.EnableTrustServerCertificate);
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);
}
this._errorMessageService.showDialog(severity, headerTitle, message, messageDetails, TelemetryView.ConnectionErrorDialog, actions, undefined, undefined);
}
}

View File

@@ -295,7 +295,7 @@ export class ConnectionDialogWidget extends Modal {
private connect(element?: IConnectionProfile): void {
this.logService.debug('ConnectionDialogWidget: Connect button is clicked');
if (this._connectButton.enabled) {
this._telemetryService.createActionEvent(TelemetryKeys.TelemetryView.ConnectionDialog, TelemetryKeys.TelemetryAction.ConnectToServer).withAdditionalProperties(
this._telemetryService.createActionEvent(TelemetryKeys.TelemetryView.ConnectionErrorDialog, TelemetryKeys.TelemetryAction.ConnectToServer).withAdditionalProperties(
{ [TelemetryKeys.TelemetryPropertyName.ConnectionSource]: this._connectionSource }
).send();
this._connecting = true;

View File

@@ -329,7 +329,7 @@ export class ConnectionManagementService extends Disposable implements IConnecti
// If the password is required and still not loaded show the dialog
if ((!foundPassword && this._connectionStore.isPasswordRequired(newConnection) && !newConnection.password) || !tokenFillSuccess) {
return this.showConnectionDialogOnError(connection, owner, { connected: false, errorMessage: undefined, callStack: undefined, errorCode: undefined }, options);
return this.showConnectionDialogOnError(connection, owner, { connected: false, errorMessage: undefined, messageDetails: undefined, errorCode: undefined }, options);
} else {
// Try to connect
return this.connectWithOptions(newConnection, owner.uri, options, owner).then(connectionResult => {
@@ -564,14 +564,22 @@ export class ConnectionManagementService extends Disposable implements IConnecti
return this.connectWithOptions(connection, uri, options, callbacks);
}
else {
let connectionErrorHandled = await this._errorDiagnosticsService.tryHandleConnectionError(connectionResult.errorCode, connectionResult.errorMessage, connection.providerName, connection);
if (connectionErrorHandled.handled) {
let connectionErrorHandleResult = await this._errorDiagnosticsService.tryHandleConnectionError(connectionResult, connection.providerName, connection);
if (connectionErrorHandleResult.handled) {
connectionResult.errorHandled = true;
if (connectionErrorHandled.options) {
if (connectionErrorHandleResult.options) {
//copy over altered connection options from the result if provided.
connection.options = connectionErrorHandled.options;
connection.options = connectionErrorHandleResult.options;
}
if (connectionErrorHandleResult.reconnect) {
// Attempt reconnect if requested by provider
return this.connectWithOptions(connection, uri, options, callbacks);
} else {
if (callbacks.onConnectCanceled) {
callbacks.onConnectCanceled();
}
return connectionResult;
}
return this.connectWithOptions(connection, uri, options, callbacks);
}
else {
// Error not handled by any registered providers so fail the connection
@@ -1299,18 +1307,18 @@ export class ConnectionManagementService extends Disposable implements IConnecti
if (connectionMngInfo && connectionMngInfo.deleted) {
this._logService.info(`Found deleted connection management info for ${uri} - removing`);
this._connectionStatusManager.deleteConnection(uri);
resolve({ connected: connectResult, errorMessage: undefined, errorCode: undefined, callStack: undefined, errorHandled: true, connectionProfile: connection });
resolve({ connected: connectResult, errorMessage: undefined, errorCode: undefined, messageDetails: undefined, errorHandled: true, connectionProfile: connection });
} else {
if (errorMessage) {
// Connection to the server failed
this._logService.info(`Error occurred while connecting, removing connection management info for ${uri}`);
this._connectionStatusManager.deleteConnection(uri);
resolve({ connected: connectResult, errorMessage: errorMessage, errorCode: errorCode, callStack: callStack, connectionProfile: connection });
resolve({ connected: connectResult, errorMessage: errorMessage, errorCode: errorCode, messageDetails: callStack, connectionProfile: connection });
} else {
if (connectionMngInfo.serverInfo) {
connection.options.isCloud = connectionMngInfo.serverInfo.isCloud;
}
resolve({ connected: connectResult, errorMessage: errorMessage, errorCode: errorCode, callStack: callStack, connectionProfile: connection });
resolve({ connected: connectResult, errorMessage: errorMessage, errorCode: errorCode, messageDetails: callStack, connectionProfile: connection });
}
}
});

View File

@@ -108,10 +108,9 @@ suite('ConnectionDialogService tests', () => {
testInstantiationService.stub(ICapabilitiesService, new TestCapabilitiesService());
let logService: ILogService = new NullLogService();
let telemetryService: IAdsTelemetryService = new NullAdsTelemetryService();
connectionDialogService = new ConnectionDialogService(testInstantiationService, capabilitiesService, errorMessageService.object,
new TestConfigurationService(), new BrowserClipboardService(layoutService, logService), NullCommandService, logService, telemetryService);
new TestConfigurationService(), new BrowserClipboardService(layoutService, logService), NullCommandService, logService);
(connectionDialogService as any)._connectionManagementService = mockConnectionManagementService.object;
let providerDisplayNames = ['Mock SQL Server'];
let providerNameToDisplayMap = { 'MSSQL': 'Mock SQL Server' };
@@ -290,7 +289,7 @@ suite('ConnectionDialogService tests', () => {
connected: false,
errorMessage: 'test_error',
errorCode: -1,
callStack: 'testCallStack'
messageDetails: 'testCallStack'
};
// promise only resolves upon handleDefaultOnConnect, must return it at the end
let connectionPromise = connectionDialogService.openDialogAndWait(mockConnectionManagementService.object, testConnectionParams, connectionProfile, connectionResult, false);

View File

@@ -203,7 +203,7 @@ suite('SQL ConnectionManagementService tests', () => {
TypeMoq.It.isAny(),
TypeMoq.It.is<INewConnectionParams>(p => p.connectionType === connectionType && (uri === undefined || p.input.uri === uri)),
TypeMoq.It.is<IConnectionProfile>(c => c !== undefined && c.serverName === connectionProfile.serverName),
connectionResult ? TypeMoq.It.is<IConnectionResult>(r => r.errorMessage === connectionResult.errorMessage && r.callStack === connectionResult.callStack) : undefined,
connectionResult ? TypeMoq.It.is<IConnectionResult>(r => r.errorMessage === connectionResult.errorMessage && r.messageDetails === connectionResult.messageDetails) : undefined,
options ? TypeMoq.It.isAny() : undefined),
didShow ? TypeMoq.Times.once() : TypeMoq.Times.never());
@@ -212,7 +212,7 @@ suite('SQL ConnectionManagementService tests', () => {
TypeMoq.It.isAny(),
TypeMoq.It.is<INewConnectionParams>(p => p.connectionType === connectionType && ((uri === undefined && p.input === undefined) || p.input.uri === uri)),
undefined,
connectionResult ? TypeMoq.It.is<IConnectionResult>(r => r.errorMessage === connectionResult.errorMessage && r.callStack === connectionResult.callStack) : undefined,
connectionResult ? TypeMoq.It.is<IConnectionResult>(r => r.errorMessage === connectionResult.errorMessage && r.messageDetails === connectionResult.messageDetails) : undefined,
options ? TypeMoq.It.isAny() : undefined),
didShow ? TypeMoq.Times.once() : TypeMoq.Times.never());
}
@@ -422,7 +422,7 @@ suite('SQL ConnectionManagementService tests', () => {
connected: expectedConnection,
errorMessage: error,
errorCode: errorCode,
callStack: errorCallStack
messageDetails: errorCallStack
};
let result = await connect(uri, options, false, connectionProfile, error, errorCode, errorCallStack);
@@ -450,7 +450,7 @@ suite('SQL ConnectionManagementService tests', () => {
connected: expectedConnection,
errorMessage: error,
errorCode: errorCode,
callStack: errorCallStack
messageDetails: errorCallStack
};
let result = await connect(uri, options, false, connectionProfile, error, errorCode, errorCallStack);
@@ -1097,7 +1097,7 @@ suite('SQL ConnectionManagementService tests', () => {
connected: expectedConnection,
errorMessage: error,
errorCode: errorCode,
callStack: errorCallStack
messageDetails: errorCallStack
};
let result = await connect(uri, options, false, connectionProfile, error, errorCode, errorCallStack);
@@ -1177,7 +1177,7 @@ suite('SQL ConnectionManagementService tests', () => {
connected: expectedConnection,
errorMessage: error,
errorCode: errorCode,
callStack: errorCallStack
messageDetails: errorCallStack
};
let result = await connect(uri, options, false, connectionProfile, error, errorCode, errorCallStack);
@@ -1209,7 +1209,7 @@ suite('SQL ConnectionManagementService tests', () => {
connected: expectedConnection,
errorMessage: error,
errorCode: errorCode,
callStack: errorCallStack
messageDetails: errorCallStack
};
let result = await connect(uri, options, false, connectionProfile, error, errorCode, errorCallStack);
@@ -1233,7 +1233,7 @@ suite('SQL ConnectionManagementService tests', () => {
connected: expectedConnection,
errorMessage: undefined,
errorCode: undefined,
callStack: undefined
messageDetails: undefined
};
let result = await connect(uri, options, false, connectionProfileWithEmptyUnsavedPassword);
@@ -1258,7 +1258,7 @@ suite('SQL ConnectionManagementService tests', () => {
connected: expectedConnection,
errorMessage: undefined,
errorCode: undefined,
callStack: undefined
messageDetails: undefined
};
let result = await connect(uri, options, false, connectionProfileWithEmptySavedPassword);
@@ -1294,7 +1294,7 @@ suite('SQL ConnectionManagementService tests', () => {
connected: expectedConnection,
errorMessage: undefined,
errorCode: undefined,
callStack: undefined
messageDetails: undefined
};
let result = await connect(uri, options, false, connectionProfileWithEmptySavedPassword);

View File

@@ -5,6 +5,7 @@
import { INewConnectionParams, IConnectionResult, IConnectionManagementService, IConnectionCompletionOptions } from 'sql/platform/connection/common/connectionManagement';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { IErrorDialogOptions } from 'sql/workbench/api/common/sqlExtHostTypes';
import { IConnectionDialogService } from 'sql/workbench/services/connection/common/connectionDialogService';
export class TestConnectionDialogService implements IConnectionDialogService {
@@ -29,4 +30,8 @@ export class TestConnectionDialogService implements IConnectionDialogService {
public callDefaultOnConnect(connection: IConnectionProfile, params: INewConnectionParams): Promise<void> {
return Promise.resolve(undefined);
}
public async showErrorDialogAsync(options: IErrorDialogOptions): Promise<string | undefined> {
return Promise.resolve(undefined);
}
}

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import { IConnectionResult } from 'sql/platform/connection/common/connectionManagement';
import { IErrorDiagnosticsService } from 'sql/workbench/services/diagnostics/common/errorDiagnosticsService';
export class TestErrorDiagnosticsService implements IErrorDiagnosticsService {
@@ -15,7 +16,7 @@ export class TestErrorDiagnosticsService implements IErrorDiagnosticsService {
unregisterDiagnosticsProvider(ProviderId: string): void {
}
tryHandleConnectionError(errorCode: number, errorMessage: string, providerId: string, connection: azdata.IConnectionProfile): Promise<azdata.diagnostics.ConnectionDiagnosticsResult> {
tryHandleConnectionError(connectionResult: IConnectionResult, providerId: string, connection: azdata.IConnectionProfile): Promise<azdata.diagnostics.ConnectionDiagnosticsResult> {
return Promise.resolve({ handled: false });
}
}