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:
Alan Ren
2019-08-14 16:33:22 -07:00
committed by GitHub
parent 3bb70a7b1f
commit 4e8c06f36d
3 changed files with 136 additions and 47 deletions

View File

@@ -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": "",

View File

@@ -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'
}

View File

@@ -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, '') };
}
}
}