mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-19 17:22:48 -05:00
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:
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user