mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-30 17:23:29 -05:00
add sql password validation (#6715)
* add sql password validation * refactor the code * correct the user name for bdc, it is also sa * comments
This commit is contained in:
@@ -113,7 +113,8 @@
|
||||
{
|
||||
"label": "%docker-sql-password-field%",
|
||||
"variableName": "AZDATA_NB_VAR_DOCKER_PASSWORD",
|
||||
"type": "password",
|
||||
"type": "sql_password",
|
||||
"userName": "sa",
|
||||
"confirmationRequired": true,
|
||||
"confirmationLabel": "%docker-confirm-sql-password-field%",
|
||||
"defaultValue": "",
|
||||
@@ -163,7 +164,8 @@
|
||||
{
|
||||
"label": "%docker-sql-password-field%",
|
||||
"variableName": "AZDATA_NB_VAR_DOCKER_PASSWORD",
|
||||
"type": "password",
|
||||
"type": "sql_password",
|
||||
"userName": "sa",
|
||||
"confirmationRequired": true,
|
||||
"confirmationLabel": "%docker-confirm-sql-password-field%",
|
||||
"defaultValue": "",
|
||||
@@ -266,7 +268,8 @@
|
||||
{
|
||||
"label": "%bdc-password-field%",
|
||||
"variableName": "AZDATA_NB_VAR_BDC_CONTROLLER_PASSWORD",
|
||||
"type": "password",
|
||||
"type": "sql_password",
|
||||
"userName": "sa",
|
||||
"confirmationRequired": true,
|
||||
"confirmationLabel": "%bdc-confirm-password-field%",
|
||||
"defaultValue": "",
|
||||
@@ -370,7 +373,8 @@
|
||||
{
|
||||
"label": "%bdc-password-field%",
|
||||
"variableName": "AZDATA_NB_VAR_BDC_CONTROLLER_PASSWORD",
|
||||
"type": "password",
|
||||
"type": "sql_password",
|
||||
"userName": "sa",
|
||||
"confirmationRequired": true,
|
||||
"confirmationLabel": "%bdc-confirm-password-field%",
|
||||
"defaultValue": "",
|
||||
@@ -421,7 +425,8 @@
|
||||
{
|
||||
"label": "%bdc-password-field%",
|
||||
"variableName": "AZDATA_NB_VAR_BDC_CONTROLLER_PASSWORD",
|
||||
"type": "password",
|
||||
"type": "sql_password",
|
||||
"userName": "sa",
|
||||
"confirmationRequired": true,
|
||||
"confirmationLabel": "%bdc-confirm-password-field%",
|
||||
"defaultValue": "",
|
||||
|
||||
@@ -62,12 +62,14 @@ export interface DialogFieldInfo {
|
||||
required: boolean;
|
||||
options: string[];
|
||||
placeHolder: string;
|
||||
userName?: string; //needed for sql server's password complexity requirement check, password can not include the login name.
|
||||
}
|
||||
|
||||
export enum FieldType {
|
||||
Text = 'text',
|
||||
Number = 'number',
|
||||
DateTimeText = 'datetime_text',
|
||||
SQLPassword = 'sql_password',
|
||||
Password = 'password',
|
||||
Options = 'options'
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ const localize = nls.loadMessageBundle();
|
||||
export class DeploymentDialog extends DialogBase {
|
||||
|
||||
private variables: { [s: string]: string | undefined; } = {};
|
||||
private validators: (() => { valid: boolean, message: string })[] = [];
|
||||
|
||||
constructor(context: vscode.ExtensionContext,
|
||||
private notebookService: INotebookService,
|
||||
@@ -46,6 +47,22 @@ export class DeploymentDialog extends DialogBase {
|
||||
);
|
||||
|
||||
const form = formBuilder.withLayout({ width: '100%' }).component();
|
||||
const self = this;
|
||||
this._dialogObject.registerCloseValidator(() => {
|
||||
const messages: string[] = [];
|
||||
self.validators.forEach(validator => {
|
||||
const result = validator();
|
||||
if (!result.valid) {
|
||||
messages.push(result.message);
|
||||
}
|
||||
});
|
||||
if (messages.length > 0) {
|
||||
self._dialogObject.message = { level: azdata.window.MessageLevel.Error, text: messages.join('\n') };
|
||||
} else {
|
||||
self._dialogObject.message = { text: '' };
|
||||
}
|
||||
return messages.length === 0;
|
||||
});
|
||||
|
||||
return view.initializeModel(form);
|
||||
});
|
||||
@@ -55,74 +72,119 @@ export class DeploymentDialog extends DialogBase {
|
||||
}
|
||||
|
||||
private addField(view: azdata.ModelView, fields: azdata.FormComponent[], fieldInfo: DialogFieldInfo): void {
|
||||
this.variables[fieldInfo.variableName] = fieldInfo.defaultValue;
|
||||
let component: { component: azdata.Component, title: string }[] | azdata.Component | undefined = undefined;
|
||||
switch (fieldInfo.type) {
|
||||
case FieldType.Options:
|
||||
this.addOptionsTypeField(view, fields, fieldInfo);
|
||||
component = this.createOptionsTypeField(view, fieldInfo);
|
||||
break;
|
||||
case FieldType.DateTimeText:
|
||||
component = this.createDateTimeTextField(view, fieldInfo);
|
||||
break;
|
||||
case FieldType.Number:
|
||||
component = this.createNumberField(view, fieldInfo);
|
||||
break;
|
||||
case FieldType.SQLPassword:
|
||||
case FieldType.Password:
|
||||
component = this.createPasswordField(view, fieldInfo);
|
||||
break;
|
||||
case FieldType.Text:
|
||||
this.addInputTypeField(view, fields, fieldInfo);
|
||||
component = this.createTextField(view, fieldInfo);
|
||||
break;
|
||||
default:
|
||||
throw new Error(localize('deploymentDialog.UnknownFieldTypeError', "Unknown field type: \"{0}\"", fieldInfo.type));
|
||||
}
|
||||
|
||||
if (component) {
|
||||
if (Array.isArray(component)) {
|
||||
fields.push(...component);
|
||||
} else {
|
||||
fields.push({ title: fieldInfo.label, component: component });
|
||||
}
|
||||
} else {
|
||||
throw new Error(localize('deploymentDialog.addFieldError', "Failed to add field: \"{0}\"", fieldInfo.label));
|
||||
}
|
||||
}
|
||||
|
||||
private addOptionsTypeField(view: azdata.ModelView, fields: azdata.FormComponent[], fieldInfo: DialogFieldInfo): void {
|
||||
const component = view.modelBuilder.dropDown().withProperties<azdata.DropDownProperties>({ values: fieldInfo.options, value: fieldInfo.defaultValue }).component();
|
||||
this.variables[fieldInfo.variableName] = fieldInfo.defaultValue;
|
||||
this._toDispose.push(component.onValueChanged(() => { this.variables[fieldInfo.variableName] = <string>component.value; }));
|
||||
fields.push({ title: fieldInfo.label, component: component });
|
||||
private createOptionsTypeField(view: azdata.ModelView, fieldInfo: DialogFieldInfo): azdata.DropDownComponent {
|
||||
const dropdown = view.modelBuilder.dropDown().withProperties<azdata.DropDownProperties>({ values: fieldInfo.options, value: fieldInfo.defaultValue }).component();
|
||||
this._toDispose.push(dropdown.onValueChanged(() => { this.variables[fieldInfo.variableName] = <string>dropdown.value; }));
|
||||
return dropdown;
|
||||
}
|
||||
|
||||
private addInputTypeField(view: azdata.ModelView, fields: azdata.FormComponent[], fieldInfo: DialogFieldInfo): void {
|
||||
let inputType: azdata.InputBoxInputType = 'text';
|
||||
let defaultValue: string | undefined = fieldInfo.defaultValue;
|
||||
|
||||
switch (fieldInfo.type) {
|
||||
case FieldType.Number:
|
||||
inputType = 'number';
|
||||
break;
|
||||
case FieldType.Password:
|
||||
inputType = 'password';
|
||||
break;
|
||||
case FieldType.DateTimeText:
|
||||
defaultValue = fieldInfo.defaultValue + new Date().toISOString().slice(0, 19).replace(/[^0-9]/g, '');
|
||||
break;
|
||||
}
|
||||
const component = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
|
||||
value: defaultValue, ariaLabel: fieldInfo.label, inputType: inputType, min: fieldInfo.min, max: fieldInfo.max, required: fieldInfo.required, placeHolder: fieldInfo.placeHolder
|
||||
private createDateTimeTextField(view: azdata.ModelView, fieldInfo: DialogFieldInfo): azdata.InputBoxComponent {
|
||||
const defaultValue = fieldInfo.defaultValue + new Date().toISOString().slice(0, 19).replace(/[^0-9]/g, '');
|
||||
const input = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
|
||||
value: defaultValue, ariaLabel: fieldInfo.label, inputType: 'text', required: fieldInfo.required, placeHolder: fieldInfo.placeHolder
|
||||
}).component();
|
||||
this.variables[fieldInfo.variableName] = defaultValue;
|
||||
this._toDispose.push(component.onTextChanged(() => { this.variables[fieldInfo.variableName] = component.value; }));
|
||||
fields.push({ title: fieldInfo.label, component: component });
|
||||
this._toDispose.push(input.onTextChanged(() => { this.variables[fieldInfo.variableName] = input.value; }));
|
||||
return input;
|
||||
|
||||
if (fieldInfo.type === FieldType.Password && fieldInfo.confirmationRequired) {
|
||||
const confirmPasswordComponent = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({ ariaLabel: fieldInfo.confirmationLabel, inputType: inputType, required: true }).component();
|
||||
fields.push({ title: fieldInfo.confirmationLabel, component: confirmPasswordComponent });
|
||||
}
|
||||
|
||||
this._dialogObject.registerCloseValidator((): boolean => {
|
||||
const passwordMatches = component.value === confirmPasswordComponent.value;
|
||||
if (!passwordMatches) {
|
||||
this._dialogObject.message = { level: azdata.window.MessageLevel.Error, text: localize('passwordNotMatch', "{0} doesn't match the confirmation password", fieldInfo.label) };
|
||||
private createNumberField(view: azdata.ModelView, fieldInfo: DialogFieldInfo): azdata.InputBoxComponent {
|
||||
const input = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
|
||||
value: fieldInfo.defaultValue, ariaLabel: fieldInfo.label, inputType: 'number', min: fieldInfo.min, max: fieldInfo.max, required: fieldInfo.required
|
||||
}).component();
|
||||
this._toDispose.push(input.onTextChanged(() => { this.variables[fieldInfo.variableName] = input.value; }));
|
||||
return input;
|
||||
}
|
||||
|
||||
private createTextField(view: azdata.ModelView, fieldInfo: DialogFieldInfo): azdata.InputBoxComponent {
|
||||
const input = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
|
||||
value: fieldInfo.defaultValue, ariaLabel: fieldInfo.label, inputType: 'text', min: fieldInfo.min, max: fieldInfo.max, required: fieldInfo.required, placeHolder: fieldInfo.placeHolder
|
||||
}).component();
|
||||
this._toDispose.push(input.onTextChanged(() => { this.variables[fieldInfo.variableName] = input.value; }));
|
||||
return input;
|
||||
}
|
||||
|
||||
private createPasswordField(view: azdata.ModelView, fieldInfo: DialogFieldInfo): { title: string, component: azdata.Component }[] {
|
||||
const components: { title: string, component: azdata.Component }[] = [];
|
||||
const passwordInput = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
|
||||
ariaLabel: fieldInfo.label, inputType: 'password', required: fieldInfo.required, placeHolder: fieldInfo.placeHolder
|
||||
}).component();
|
||||
this._toDispose.push(passwordInput.onTextChanged(() => { this.variables[fieldInfo.variableName] = passwordInput.value; }));
|
||||
components.push({ title: fieldInfo.label, component: passwordInput });
|
||||
|
||||
if (fieldInfo.type === FieldType.SQLPassword) {
|
||||
const invalidPasswordMessage = localize('invalidSQLPassword', "{0} doesn't meet the password complexity requirement. For more information: https://docs.microsoft.com/sql/relational-databases/security/password-policy", fieldInfo.label);
|
||||
this._toDispose.push(passwordInput.onTextChanged(() => {
|
||||
if (fieldInfo.type === FieldType.SQLPassword && this.isValidSQLPassword(fieldInfo, passwordInput)) {
|
||||
this.removeValidationMessage(invalidPasswordMessage);
|
||||
}
|
||||
return passwordMatches;
|
||||
}));
|
||||
|
||||
this.validators.push((): { valid: boolean, message: string } => {
|
||||
return { valid: this.isValidSQLPassword(fieldInfo, passwordInput), message: invalidPasswordMessage };
|
||||
});
|
||||
}
|
||||
|
||||
if (fieldInfo.confirmationRequired) {
|
||||
const passwordNotMatchMessage = localize('passwordNotMatch', "{0} doesn't match the confirmation password", fieldInfo.label);
|
||||
|
||||
const confirmPasswordInput = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({ ariaLabel: fieldInfo.confirmationLabel, inputType: 'password', required: true }).component();
|
||||
components.push({ title: fieldInfo.confirmationLabel, component: confirmPasswordInput });
|
||||
|
||||
this.validators.push((): { valid: boolean, message: string } => {
|
||||
const passwordMatches = passwordInput.value === confirmPasswordInput.value;
|
||||
return { valid: passwordMatches, message: passwordNotMatchMessage };
|
||||
});
|
||||
|
||||
const checkPassword = (): void => {
|
||||
const passwordMatches = component.value === confirmPasswordComponent.value;
|
||||
if (passwordMatches) {
|
||||
this._dialogObject.message = { text: '' };
|
||||
const updatePasswordMismatchMessage = () => {
|
||||
if (passwordInput.value === confirmPasswordInput.value) {
|
||||
this.removeValidationMessage(passwordNotMatchMessage);
|
||||
}
|
||||
};
|
||||
|
||||
this._toDispose.push(component.onTextChanged(() => {
|
||||
checkPassword();
|
||||
this._toDispose.push(passwordInput.onTextChanged(() => {
|
||||
updatePasswordMismatchMessage();
|
||||
}));
|
||||
this._toDispose.push(confirmPasswordComponent.onTextChanged(() => {
|
||||
checkPassword();
|
||||
this._toDispose.push(confirmPasswordInput.onTextChanged(() => {
|
||||
updatePasswordMismatchMessage();
|
||||
}));
|
||||
}
|
||||
return components;
|
||||
}
|
||||
|
||||
private onComplete(): void {
|
||||
@@ -132,4 +194,24 @@ export class DeploymentDialog extends DialogBase {
|
||||
this.notebookService.launchNotebook(this.deploymentProvider.notebook);
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
private isValidSQLPassword(field: DialogFieldInfo, component: azdata.InputBoxComponent): boolean {
|
||||
const password = component.value!;
|
||||
// Validate SQL Server password
|
||||
const containsUserName = password && field.userName && password.toUpperCase().includes(field.userName.toUpperCase());
|
||||
// Instead of using one RegEx, I am seperating it to make it more readable.
|
||||
const hasUpperCase = /[A-Z]/.test(password) ? 1 : 0;
|
||||
const hasLowerCase = /[a-z]/.test(password) ? 1 : 0;
|
||||
const hasNumbers = /\d/.test(password) ? 1 : 0;
|
||||
const hasNonalphas = /\W/.test(password) ? 1 : 0;
|
||||
return !containsUserName && password.length >= 8 && password.length <= 128 && (hasUpperCase + hasLowerCase + hasNumbers + hasNonalphas >= 3);
|
||||
}
|
||||
|
||||
private removeValidationMessage(message: string): void {
|
||||
if (this._dialogObject.message && this._dialogObject.message.text.includes(message)) {
|
||||
const messageWithLineBreak = message + '\n';
|
||||
const searchText = this._dialogObject.message.text.includes(messageWithLineBreak) ? messageWithLineBreak : message;
|
||||
this._dialogObject.message = { text: this._dialogObject.message.text.replace(searchText, '') };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user