Added reset password dialog upon SQL Server expired password error (#21295)

* Added initial password connection dialog box

* made small changes

* more preliminary work

* more WIP changes

* more cleanup done

* added dialog instantiation

* added placeholder dialog window

* added changePasswordController

* made some changes to changePasswordController

* some changes made

* added more changes

* more changes made to dialogue

* added password confirm box

* added WIP change password function

* small changes made to API

* small changes for test

* added  uri

* added valid password

* added TODO comments

* added small change to connectionManagementService

* added connectionManagementService password change

* added comment on what to do next

* made some simplification of change password

* added response callback

* added fixes to protocol

* added throw error for passwordChangeResult

* WIP added call to self after password change

* WIP changes to implementing new password change dialog

* added changes to passwordChangeDialog

* added launchChangePasswordDialog

* remove erroneous css

* added working dialog

* removed old password change dialog

* fixed space

* added checkbox option to passwordChangeDialog

* added test signatures

* added error handling

* added some changes

* added changes to HTML for passwordChangeDialog

* added CSS to passwordChangeDialog

* added display none for matching passwords

* added documentation changes

* small cleanup

* added working error catch and retry

* added await

* added recovery instructions

* Added ok button hide for button click.

* added loading spinner

* fixed for semicolon

* added updated message

* Added message change

* added minor fixes

* added small fixes

* made more changes

* renamed messages to errorDetails

* added styling to passwordChangeDialog

* simplified error message

* changed comment

* modified azdata to be consistent

* small changes

* change to azdata for consistency

* added clarification for provider

* removed additional instructions

* Added new dialog title

* addressed feedback

* added comments

* added changes
This commit is contained in:
Alex Ma
2022-12-07 14:27:01 -08:00
committed by GitHub
parent db329049ff
commit cffba368a9
14 changed files with 275 additions and 0 deletions

View File

@@ -83,6 +83,9 @@ export class MainThreadDataProtocol extends Disposable implements MainThreadData
disconnect(connectionUri: string): Thenable<boolean> {
return self._proxy.$disconnect(handle, connectionUri);
},
changePassword(connectionUri, connectionInfo, newPassword): Thenable<azdata.PasswordChangeResult> {
return self._proxy.$changePassword(handle, connectionUri, connectionInfo, newPassword);
},
changeDatabase(connectionUri: string, newDatabase: string): Thenable<boolean> {
return self._proxy.$changeDatabase(handle, connectionUri, newDatabase);
},

View File

@@ -225,6 +225,13 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape {
return this._resolveProvider<azdata.ConnectionProvider>(handle).disconnect(connectionUri);
}
override $changePassword(handle: number, connectionUri: string, connection: azdata.ConnectionInfo, newPassword: string): Thenable<azdata.PasswordChangeResult> {
if (this.uriTransformer) {
connectionUri = this._getTransformedUri(connectionUri, this.uriTransformer.transformIncoming);
}
return this._resolveProvider<azdata.ConnectionProvider>(handle).changePassword(connectionUri, connection, newPassword);
}
override $cancelConnect(handle: number, connectionUri: string): Thenable<boolean> {
return this._resolveProvider<azdata.ConnectionProvider>(handle).cancelConnect(connectionUri);
}

View File

@@ -67,6 +67,11 @@ export abstract class ExtHostDataProtocolShape {
*/
$disconnect(handle: number, connectionUri: string): Thenable<boolean> { throw ni(); }
/**
* Changes password of the connection profile's user.
*/
$changePassword(handle: number, connectionUri: string, connection: azdata.ConnectionInfo, newPassword: string): Thenable<azdata.PasswordChangeResult> { throw ni(); }
/**
* Cancel a connection to a data source using the provided connectionUri string.
*/

View File

@@ -30,6 +30,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
import { CmsConnectionController } from 'sql/workbench/services/connection/browser/cmsConnectionController';
import { PasswordChangeDialog } from 'sql/workbench/services/connection/browser/passwordChangeDialog';
import { entries } from 'sql/base/common/collections';
import { onUnexpectedError } from 'vs/base/common/errors';
import { ILogService } from 'vs/platform/log/common/log';
@@ -241,6 +242,14 @@ export class ConnectionDialogService implements IConnectionDialogService {
}
}
/**
* Calls the default connect function (used by password reset dialog)
*/
public async callDefaultOnConnect(connection: IConnectionProfile, params: INewConnectionParams): Promise<void> {
// Needed for password reset dialog to connect after changing password.
return this.handleDefaultOnConnect(params, connection);
}
private async handleDefaultOnConnect(params: INewConnectionParams, connection: IConnectionProfile): Promise<void> {
if (this.ignoreNextConnect) {
this._connectionDialog.resetConnection();
@@ -275,6 +284,9 @@ export class ConnectionDialogService implements IConnectionDialogService {
} else if (connectionResult && connectionResult.errorHandled) {
this._connectionDialog.resetConnection();
this._logService.debug(`ConnectionDialogService: Error handled and connection reset - Error: ${connectionResult.errorMessage}`);
} else if (connection.providerName === Constants.mssqlProviderName && connectionResult.errorCode === Constants.sqlPasswordErrorCode) {
this._connectionDialog.resetConnection();
this.launchChangePasswordDialog(connection, params);
} else {
this._connectionDialog.resetConnection();
this.showErrorDialog(Severity.Error, this._connectionErrorTitle, connectionResult.errorMessage, connectionResult.callStack, connectionResult.errorCode);
@@ -495,6 +507,12 @@ export class ConnectionDialogService implements IConnectionDialogService {
recentConnections.forEach(conn => conn.dispose());
}
public launchChangePasswordDialog(profile: IConnectionProfile, params: INewConnectionParams): void {
let dialog = this._instantiationService.createInstance(PasswordChangeDialog);
dialog.open(profile, params);
}
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

View File

@@ -425,6 +425,14 @@ export class ConnectionManagementService extends Disposable implements IConnecti
}
}
/**
* Changes password of the connection profile's user.
*/
public changePassword(connection: interfaces.IConnectionProfile, uri: string, newPassword: string):
Promise<azdata.PasswordChangeResult> {
return this.sendChangePasswordRequest(connection, uri, newPassword);
}
/**
* Opens a new connection and saves the profile in the settings.
* This method doesn't load the password because it only gets called from the
@@ -1037,6 +1045,18 @@ export class ConnectionManagementService extends Disposable implements IConnecti
});
}
private async sendChangePasswordRequest(connection: interfaces.IConnectionProfile, uri: string, newPassword: string): Promise<azdata.PasswordChangeResult> {
let connectionInfo = Object.assign({}, {
options: connection.options
});
return this._providers.get(connection.providerName).onReady.then((provider) => {
return provider.changePassword(uri, connectionInfo, newPassword).then(result => {
return result;
})
});
}
private sendCancelRequest(uri: string): Promise<boolean> {
let providerId: string = this.getProviderIdFromUri(uri);
if (!providerId) {

View File

@@ -0,0 +1,31 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.change-password-dialog {
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
.change-password-dialog .properties-content {
flex: 1 1 auto;
overflow-y: auto;
}
.change-password-dialog .component-label {
vertical-align: middle;
}
.change-password-dialog .components-grid {
display: grid;
/* grid-template-columns: column 1 is for label, column 2 is for component.*/
grid-template-columns: max-content 1fr;
grid-template-rows: max-content;
grid-gap: 10px;
padding: 5px;
align-content: start;
box-sizing: border-box;
}

View File

@@ -0,0 +1,147 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/passwordDialog';
import { Button } from 'sql/base/browser/ui/button/button';
import { Modal } from 'sql/workbench/browser/modal/modal';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { attachInputBoxStyler } from 'sql/platform/theme/common/styler';
import { INewConnectionParams } from 'sql/platform/connection/common/connectionManagement';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { localize } from 'vs/nls';
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import { attachButtonStyler } from 'vs/platform/theme/common/styler';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import * as DOM from 'vs/base/browser/dom';
import { ILogService } from 'vs/platform/log/common/log';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
import { IConnectionDialogService } from 'sql/workbench/services/connection/common/connectionDialogService';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import Severity from 'vs/base/common/severity';
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
import { attachModalDialogStyler } from 'sql/workbench/common/styler';
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfiguration';
const dialogWidth: string = '300px'; // Width is set manually here as there is no default width for normal dialogs.
const okText: string = localize('passwordChangeDialog.ok', "OK");
const cancelText: string = localize('passwordChangeDialog.cancel', "Cancel");
const dialogTitle: string = localize('passwordChangeDialog.title', "Change Password");
const newPasswordText: string = localize('passwordChangeDialog.newPassword', 'New password:');
const confirmPasswordText: string = localize('passwordChangeDialog.confirmPassword', 'Confirm password:');
const passwordChangeLoadText: string = localize('passwordChangeDialog.connecting', "Connecting");
const errorHeader: string = localize('passwordChangeDialog.errorHeader', "Failure when attempting to change password");
const errorPasswordMismatchMessage = localize('passwordChangeDialog.errorPasswordMismatchMessage', "Passwords entered do not match\n\nPress OK and enter the exact same password in both boxes.");
export class PasswordChangeDialog extends Modal {
private _okButton?: Button;
private _cancelButton?: Button;
private _profile: IConnectionProfile;
private _params: INewConnectionParams;
private _uri: string;
private _passwordValueText: InputBox;
private _confirmValueText: InputBox;
constructor(
@IThemeService themeService: IThemeService,
@IClipboardService clipboardService: IClipboardService,
@IConnectionManagementService private readonly connectionManagementService: IConnectionManagementService,
@IErrorMessageService private readonly errorMessageService: IErrorMessageService,
@IConnectionDialogService private readonly connectionDialogService: IConnectionDialogService,
@ILayoutService layoutService: ILayoutService,
@IAdsTelemetryService telemetryService: IAdsTelemetryService,
@IContextKeyService contextKeyService: IContextKeyService,
@ILogService logService: ILogService,
@IContextViewService private readonly contextViewService: IContextViewService,
@ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService,
) {
super('', '', telemetryService, layoutService, clipboardService, themeService, logService, textResourcePropertiesService, contextKeyService, { hasSpinner: true, spinnerTitle: passwordChangeLoadText, dialogStyle: 'normal', width: dialogWidth, dialogPosition: 'left' });
}
public open(profile: IConnectionProfile, params: INewConnectionParams) {
this._profile = profile;
this._params = params;
this._uri = this.connectionManagementService.getConnectionUri(profile);
this.render();
this.show();
this._okButton!.focus();
}
public override dispose(): void {
}
public override render() {
super.render();
this.title = dialogTitle;
this._register(attachModalDialogStyler(this, this._themeService));
this._okButton = this.addFooterButton(okText, () => this.handleOkButtonClick());
this._cancelButton = this.addFooterButton(cancelText, () => this.hide('cancel'), 'right', true);
this._register(attachButtonStyler(this._okButton, this._themeService));
this._register(attachButtonStyler(this._cancelButton, this._themeService));
}
protected renderBody(container: HTMLElement) {
const body = container.appendChild(DOM.$('.change-password-dialog'));
const contentElement = body.appendChild(DOM.$('.properties-content.components-grid'));
contentElement.appendChild(DOM.$('')).appendChild(DOM.$('span.component-label')).innerText = newPasswordText;
const passwordInputContainer = contentElement.appendChild(DOM.$(''));
this._passwordValueText = new InputBox(passwordInputContainer, this.contextViewService, { type: 'password' });
this._register(attachInputBoxStyler(this._passwordValueText, this._themeService));
contentElement.appendChild(DOM.$('')).appendChild(DOM.$('span.component-label')).innerText = confirmPasswordText;
const confirmInputContainer = contentElement.appendChild(DOM.$(''));
this._confirmValueText = new InputBox(confirmInputContainer, this.contextViewService, { type: 'password' });
this._register(attachInputBoxStyler(this._confirmValueText, this._themeService));
}
protected layout(height?: number): void {
// Nothing to re-layout
}
/* espace key */
protected override onClose() {
this.hide('close');
}
/* enter key */
protected override onAccept() {
this.handleOkButtonClick();
}
private handleOkButtonClick(): void {
this._okButton.enabled = false;
this._cancelButton.enabled = false;
this.spinner = true;
this.changePasswordFunction(this._profile, this._params, this._uri, this._passwordValueText.value, this._confirmValueText.value).then(
() => {
this.hide('ok'); /* password changed successfully */
},
() => {
this._okButton.enabled = true; /* ignore, user must try again */
this._cancelButton.enabled = true;
this.spinner = false;
}
);
}
private async changePasswordFunction(connection: IConnectionProfile, params: INewConnectionParams, uri: string, oldPassword: string, newPassword: string): Promise<void> {
// Verify passwords match before changing the password.
if (oldPassword !== newPassword) {
this.errorMessageService.showDialog(Severity.Error, errorHeader, errorPasswordMismatchMessage);
return Promise.reject(new Error(errorPasswordMismatchMessage));
}
let passwordChangeResult = await this.connectionManagementService.changePassword(connection, uri, newPassword);
if (!passwordChangeResult.result) {
this.errorMessageService.showDialog(Severity.Error, errorHeader, passwordChangeResult.errorMessage);
return Promise.reject(new Error(passwordChangeResult.errorMessage));
}
connection.options['password'] = newPassword;
await this.connectionDialogService.callDefaultOnConnect(connection, params);
}
}

View File

@@ -20,4 +20,9 @@ export interface IConnectionDialogService {
* or dialog is closed
*/
openDialogAndWait(connectionManagementService: IConnectionManagementService, params?: INewConnectionParams, model?: IConnectionProfile, connectionResult?: IConnectionResult, doConnect?: boolean): Promise<IConnectionProfile>;
/**
* Calls the default connect function (used by password reset dialog)
*/
callDefaultOnConnect(connection: IConnectionProfile, params: INewConnectionParams): Promise<void>;
}

View File

@@ -25,4 +25,8 @@ export class TestConnectionDialogService implements IConnectionDialogService {
params?: INewConnectionParams, model?: IConnectionProfile, connectionResult?: IConnectionResult): Promise<IConnectionProfile> {
return Promise.resolve(undefined);
}
public callDefaultOnConnect(connection: IConnectionProfile, params: INewConnectionParams): Promise<void> {
return Promise.resolve(undefined);
}
}