mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-17 02:51:36 -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%",
|
"label": "%docker-sql-password-field%",
|
||||||
"variableName": "AZDATA_NB_VAR_DOCKER_PASSWORD",
|
"variableName": "AZDATA_NB_VAR_DOCKER_PASSWORD",
|
||||||
"type": "password",
|
"type": "sql_password",
|
||||||
|
"userName": "sa",
|
||||||
"confirmationRequired": true,
|
"confirmationRequired": true,
|
||||||
"confirmationLabel": "%docker-confirm-sql-password-field%",
|
"confirmationLabel": "%docker-confirm-sql-password-field%",
|
||||||
"defaultValue": "",
|
"defaultValue": "",
|
||||||
@@ -163,7 +164,8 @@
|
|||||||
{
|
{
|
||||||
"label": "%docker-sql-password-field%",
|
"label": "%docker-sql-password-field%",
|
||||||
"variableName": "AZDATA_NB_VAR_DOCKER_PASSWORD",
|
"variableName": "AZDATA_NB_VAR_DOCKER_PASSWORD",
|
||||||
"type": "password",
|
"type": "sql_password",
|
||||||
|
"userName": "sa",
|
||||||
"confirmationRequired": true,
|
"confirmationRequired": true,
|
||||||
"confirmationLabel": "%docker-confirm-sql-password-field%",
|
"confirmationLabel": "%docker-confirm-sql-password-field%",
|
||||||
"defaultValue": "",
|
"defaultValue": "",
|
||||||
@@ -266,7 +268,8 @@
|
|||||||
{
|
{
|
||||||
"label": "%bdc-password-field%",
|
"label": "%bdc-password-field%",
|
||||||
"variableName": "AZDATA_NB_VAR_BDC_CONTROLLER_PASSWORD",
|
"variableName": "AZDATA_NB_VAR_BDC_CONTROLLER_PASSWORD",
|
||||||
"type": "password",
|
"type": "sql_password",
|
||||||
|
"userName": "sa",
|
||||||
"confirmationRequired": true,
|
"confirmationRequired": true,
|
||||||
"confirmationLabel": "%bdc-confirm-password-field%",
|
"confirmationLabel": "%bdc-confirm-password-field%",
|
||||||
"defaultValue": "",
|
"defaultValue": "",
|
||||||
@@ -370,7 +373,8 @@
|
|||||||
{
|
{
|
||||||
"label": "%bdc-password-field%",
|
"label": "%bdc-password-field%",
|
||||||
"variableName": "AZDATA_NB_VAR_BDC_CONTROLLER_PASSWORD",
|
"variableName": "AZDATA_NB_VAR_BDC_CONTROLLER_PASSWORD",
|
||||||
"type": "password",
|
"type": "sql_password",
|
||||||
|
"userName": "sa",
|
||||||
"confirmationRequired": true,
|
"confirmationRequired": true,
|
||||||
"confirmationLabel": "%bdc-confirm-password-field%",
|
"confirmationLabel": "%bdc-confirm-password-field%",
|
||||||
"defaultValue": "",
|
"defaultValue": "",
|
||||||
@@ -421,7 +425,8 @@
|
|||||||
{
|
{
|
||||||
"label": "%bdc-password-field%",
|
"label": "%bdc-password-field%",
|
||||||
"variableName": "AZDATA_NB_VAR_BDC_CONTROLLER_PASSWORD",
|
"variableName": "AZDATA_NB_VAR_BDC_CONTROLLER_PASSWORD",
|
||||||
"type": "password",
|
"type": "sql_password",
|
||||||
|
"userName": "sa",
|
||||||
"confirmationRequired": true,
|
"confirmationRequired": true,
|
||||||
"confirmationLabel": "%bdc-confirm-password-field%",
|
"confirmationLabel": "%bdc-confirm-password-field%",
|
||||||
"defaultValue": "",
|
"defaultValue": "",
|
||||||
|
|||||||
@@ -62,12 +62,14 @@ export interface DialogFieldInfo {
|
|||||||
required: boolean;
|
required: boolean;
|
||||||
options: string[];
|
options: string[];
|
||||||
placeHolder: string;
|
placeHolder: string;
|
||||||
|
userName?: string; //needed for sql server's password complexity requirement check, password can not include the login name.
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum FieldType {
|
export enum FieldType {
|
||||||
Text = 'text',
|
Text = 'text',
|
||||||
Number = 'number',
|
Number = 'number',
|
||||||
DateTimeText = 'datetime_text',
|
DateTimeText = 'datetime_text',
|
||||||
|
SQLPassword = 'sql_password',
|
||||||
Password = 'password',
|
Password = 'password',
|
||||||
Options = 'options'
|
Options = 'options'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const localize = nls.loadMessageBundle();
|
|||||||
export class DeploymentDialog extends DialogBase {
|
export class DeploymentDialog extends DialogBase {
|
||||||
|
|
||||||
private variables: { [s: string]: string | undefined; } = {};
|
private variables: { [s: string]: string | undefined; } = {};
|
||||||
|
private validators: (() => { valid: boolean, message: string })[] = [];
|
||||||
|
|
||||||
constructor(context: vscode.ExtensionContext,
|
constructor(context: vscode.ExtensionContext,
|
||||||
private notebookService: INotebookService,
|
private notebookService: INotebookService,
|
||||||
@@ -46,6 +47,22 @@ export class DeploymentDialog extends DialogBase {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const form = formBuilder.withLayout({ width: '100%' }).component();
|
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);
|
return view.initializeModel(form);
|
||||||
});
|
});
|
||||||
@@ -55,74 +72,119 @@ export class DeploymentDialog extends DialogBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private addField(view: azdata.ModelView, fields: azdata.FormComponent[], fieldInfo: DialogFieldInfo): void {
|
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) {
|
switch (fieldInfo.type) {
|
||||||
case FieldType.Options:
|
case FieldType.Options:
|
||||||
this.addOptionsTypeField(view, fields, fieldInfo);
|
component = this.createOptionsTypeField(view, fieldInfo);
|
||||||
break;
|
break;
|
||||||
case FieldType.DateTimeText:
|
case FieldType.DateTimeText:
|
||||||
|
component = this.createDateTimeTextField(view, fieldInfo);
|
||||||
|
break;
|
||||||
case FieldType.Number:
|
case FieldType.Number:
|
||||||
|
component = this.createNumberField(view, fieldInfo);
|
||||||
|
break;
|
||||||
|
case FieldType.SQLPassword:
|
||||||
case FieldType.Password:
|
case FieldType.Password:
|
||||||
|
component = this.createPasswordField(view, fieldInfo);
|
||||||
|
break;
|
||||||
case FieldType.Text:
|
case FieldType.Text:
|
||||||
this.addInputTypeField(view, fields, fieldInfo);
|
component = this.createTextField(view, fieldInfo);
|
||||||
break;
|
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 {
|
private createOptionsTypeField(view: azdata.ModelView, fieldInfo: DialogFieldInfo): azdata.DropDownComponent {
|
||||||
const component = view.modelBuilder.dropDown().withProperties<azdata.DropDownProperties>({ values: fieldInfo.options, value: fieldInfo.defaultValue }).component();
|
const dropdown = view.modelBuilder.dropDown().withProperties<azdata.DropDownProperties>({ values: fieldInfo.options, value: fieldInfo.defaultValue }).component();
|
||||||
this.variables[fieldInfo.variableName] = fieldInfo.defaultValue;
|
this._toDispose.push(dropdown.onValueChanged(() => { this.variables[fieldInfo.variableName] = <string>dropdown.value; }));
|
||||||
this._toDispose.push(component.onValueChanged(() => { this.variables[fieldInfo.variableName] = <string>component.value; }));
|
return dropdown;
|
||||||
fields.push({ title: fieldInfo.label, component: component });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private addInputTypeField(view: azdata.ModelView, fields: azdata.FormComponent[], fieldInfo: DialogFieldInfo): void {
|
private createDateTimeTextField(view: azdata.ModelView, fieldInfo: DialogFieldInfo): azdata.InputBoxComponent {
|
||||||
let inputType: azdata.InputBoxInputType = 'text';
|
const defaultValue = fieldInfo.defaultValue + new Date().toISOString().slice(0, 19).replace(/[^0-9]/g, '');
|
||||||
let defaultValue: string | undefined = fieldInfo.defaultValue;
|
const input = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
|
||||||
|
value: defaultValue, ariaLabel: fieldInfo.label, inputType: 'text', required: fieldInfo.required, placeHolder: fieldInfo.placeHolder
|
||||||
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
|
|
||||||
}).component();
|
}).component();
|
||||||
this.variables[fieldInfo.variableName] = defaultValue;
|
this.variables[fieldInfo.variableName] = defaultValue;
|
||||||
this._toDispose.push(component.onTextChanged(() => { this.variables[fieldInfo.variableName] = component.value; }));
|
this._toDispose.push(input.onTextChanged(() => { this.variables[fieldInfo.variableName] = input.value; }));
|
||||||
fields.push({ title: fieldInfo.label, component: component });
|
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 => {
|
private createNumberField(view: azdata.ModelView, fieldInfo: DialogFieldInfo): azdata.InputBoxComponent {
|
||||||
const passwordMatches = component.value === confirmPasswordComponent.value;
|
const input = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
|
||||||
if (!passwordMatches) {
|
value: fieldInfo.defaultValue, ariaLabel: fieldInfo.label, inputType: 'number', min: fieldInfo.min, max: fieldInfo.max, required: fieldInfo.required
|
||||||
this._dialogObject.message = { level: azdata.window.MessageLevel.Error, text: localize('passwordNotMatch', "{0} doesn't match the confirmation password", fieldInfo.label) };
|
}).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 updatePasswordMismatchMessage = () => {
|
||||||
const passwordMatches = component.value === confirmPasswordComponent.value;
|
if (passwordInput.value === confirmPasswordInput.value) {
|
||||||
if (passwordMatches) {
|
this.removeValidationMessage(passwordNotMatchMessage);
|
||||||
this._dialogObject.message = { text: '' };
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this._toDispose.push(component.onTextChanged(() => {
|
this._toDispose.push(passwordInput.onTextChanged(() => {
|
||||||
checkPassword();
|
updatePasswordMismatchMessage();
|
||||||
}));
|
}));
|
||||||
this._toDispose.push(confirmPasswordComponent.onTextChanged(() => {
|
this._toDispose.push(confirmPasswordInput.onTextChanged(() => {
|
||||||
checkPassword();
|
updatePasswordMismatchMessage();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
return components;
|
||||||
}
|
}
|
||||||
|
|
||||||
private onComplete(): void {
|
private onComplete(): void {
|
||||||
@@ -132,4 +194,24 @@ export class DeploymentDialog extends DialogBase {
|
|||||||
this.notebookService.launchNotebook(this.deploymentProvider.notebook);
|
this.notebookService.launchNotebook(this.deploymentProvider.notebook);
|
||||||
this.dispose();
|
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