Add support for Login and User management (#21981)

* initial commit

* leave only march release objects

* clean up

* login dialog

* localize and use background operation

* code cleanup

* remove tab

* support server role in login

* remove canEditName

* add user support

* comments and bug fixes

* remove hasDBAccess for now

* refactoring

* fix error

* user dialog UI

* telemetry, error handling and refactoring

* Fix references to dialogInfo (#21914)

* update telemetry

* Bump STS and use actual object management service

* add preview and handle no-change scenario

* fix merge issue

---------

Co-authored-by: Karl Burtram <karlb@microsoft.com>
This commit is contained in:
Alan Ren
2023-02-17 18:02:31 -08:00
committed by GitHub
parent 6231df85e0
commit b5ce7af090
16 changed files with 2169 additions and 13 deletions

View File

@@ -0,0 +1,153 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AppContext } from '../appContext';
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { LoginDialog } from './ui/loginDialog';
import { TestObjectManagementService } from './objectManagementService';
import { getErrorMessage } from '../utils';
import { NodeType, TelemetryActions, TelemetryViews } from './constants';
import * as localizedConstants from './localizedConstants';
import { UserDialog } from './ui/userDialog';
import { IObjectManagementService } from 'mssql';
import * as constants from '../constants';
import { getNodeTypeDisplayName, refreshParentNode } from './utils';
import { TelemetryReporter } from '../telemetry';
export function registerObjectManagementCommands(appContext: AppContext) {
// Notes: Change the second parameter to false to use the actual object management service.
const service = getObjectManagementService(appContext, false);
appContext.extensionContext.subscriptions.push(vscode.commands.registerCommand('mssql.newLogin', async (context: azdata.ObjectExplorerContext) => {
await handleNewLoginDialogCommand(context, service);
}));
appContext.extensionContext.subscriptions.push(vscode.commands.registerCommand('mssql.newUser', async (context: azdata.ObjectExplorerContext) => {
await handleNewUserDialogCommand(context, service);
}));
appContext.extensionContext.subscriptions.push(vscode.commands.registerCommand('mssql.objectProperties', async (context: azdata.ObjectExplorerContext) => {
await handleObjectPropertiesDialogCommand(context, service);
}));
appContext.extensionContext.subscriptions.push(vscode.commands.registerCommand('mssql.deleteObject', async (context: azdata.ObjectExplorerContext) => {
await handleDeleteObjectCommand(context, service);
}));
}
function getObjectManagementService(appContext: AppContext, useTestService: boolean): IObjectManagementService {
if (useTestService) {
return new TestObjectManagementService();
} else {
return appContext.getService<IObjectManagementService>(constants.ObjectManagementService);
}
}
async function handleNewLoginDialogCommand(context: azdata.ObjectExplorerContext, service: IObjectManagementService): Promise<void> {
try {
const connectionUri = await azdata.connection.getUriForConnection(context.connectionProfile.id);
const dialog = new LoginDialog(service, connectionUri, true, undefined, context);
await dialog.open();
}
catch (err) {
TelemetryReporter.createErrorEvent(TelemetryViews.ObjectManagement, TelemetryActions.OpenNewObjectDialog).withAdditionalProperties({
objectType: NodeType.Login
}).send();
await vscode.window.showErrorMessage(localizedConstants.OpenNewObjectDialogError(localizedConstants.LoginTypeDisplayName, getErrorMessage(err)));
}
}
async function handleNewUserDialogCommand(context: azdata.ObjectExplorerContext, service: IObjectManagementService): Promise<void> {
try {
const connectionUri = await azdata.connection.getUriForConnection(context.connectionProfile.id);
const dialog = new UserDialog(service, connectionUri, context.connectionProfile.databaseName, true, undefined, context);
await dialog.open();
}
catch (err) {
TelemetryReporter.createErrorEvent(TelemetryViews.ObjectManagement, TelemetryActions.OpenNewObjectDialog).withAdditionalProperties({
objectType: NodeType.User
}).send();
await vscode.window.showErrorMessage(localizedConstants.OpenNewObjectDialogError(localizedConstants.UserTypeDisplayName, getErrorMessage(err)));
}
}
async function handleObjectPropertiesDialogCommand(context: azdata.ObjectExplorerContext, service: IObjectManagementService): Promise<void> {
const nodeTypeDisplayName = getNodeTypeDisplayName(context.nodeInfo.nodeType);
try {
const connectionUri = await azdata.connection.getUriForConnection(context.connectionProfile.id);
let dialog;
switch (context.nodeInfo.nodeType) {
case NodeType.Login:
dialog = new LoginDialog(service, connectionUri, false, context.nodeInfo.label);
break;
case NodeType.User:
dialog = new UserDialog(service, connectionUri, context.connectionProfile.databaseName, false, context.nodeInfo.label);
break;
default:
break;
}
if (dialog) {
await dialog.open();
}
}
catch (err) {
TelemetryReporter.createErrorEvent(TelemetryViews.ObjectManagement, TelemetryActions.OpenPropertiesDialog).withAdditionalProperties({
objectType: context.nodeInfo.nodeType
}).send();
await vscode.window.showErrorMessage(localizedConstants.OpenObjectPropertiesDialogError(nodeTypeDisplayName, context.nodeInfo.label, getErrorMessage(err)));
}
}
async function handleDeleteObjectCommand(context: azdata.ObjectExplorerContext, service: IObjectManagementService): Promise<void> {
let additionalConfirmationMessage: string;
switch (context.nodeInfo.nodeType) {
case NodeType.Login:
additionalConfirmationMessage = localizedConstants.DeleteLoginConfirmationText;
break;
default:
break;
}
const nodeTypeDisplayName = getNodeTypeDisplayName(context.nodeInfo.nodeType);
let confirmMessage = localizedConstants.DeleteObjectConfirmationText(nodeTypeDisplayName, context.nodeInfo.label);
if (additionalConfirmationMessage) {
confirmMessage = `${additionalConfirmationMessage} ${confirmMessage}`;
}
const confirmResult = await vscode.window.showWarningMessage(confirmMessage, { modal: true }, localizedConstants.YesText);
if (confirmResult !== localizedConstants.YesText) {
return;
}
azdata.tasks.startBackgroundOperation({
displayName: localizedConstants.DeleteObjectOperationDisplayName(nodeTypeDisplayName, context.nodeInfo.label),
description: '',
isCancelable: false,
operation: async (operation) => {
try {
const startTime = Date.now();
const connectionUri = await azdata.connection.getUriForConnection(context.connectionProfile.id);
switch (context.nodeInfo.nodeType) {
case NodeType.Login:
await service.deleteLogin(connectionUri, context.nodeInfo.label);
break;
case NodeType.User:
await service.deleteUser(connectionUri, context.connectionProfile.databaseName, context.nodeInfo.label);
break;
default:
return;
}
TelemetryReporter.sendTelemetryEvent(TelemetryActions.DeleteObject, {
objectType: context.nodeInfo.nodeType
}, {
ellapsedTime: Date.now() - startTime
});
}
catch (err) {
operation.updateStatus(azdata.TaskStatus.Failed, localizedConstants.DeleteObjectError(nodeTypeDisplayName, context.nodeInfo.label, getErrorMessage(err)));
TelemetryReporter.createErrorEvent(TelemetryViews.ObjectManagement, TelemetryActions.DeleteObject).withAdditionalProperties({
objectType: context.nodeInfo.nodeType
}).send();
return;
}
await refreshParentNode(context);
operation.updateStatus(azdata.TaskStatus.Succeeded);
}
});
}

View File

@@ -0,0 +1,62 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* The object types in object explorer's node context.
*/
export enum NodeType {
Login = 'ServerLevelLogin',
User = 'User'
}
export const PublicServerRoleName = 'public';
/**
* User types.
*/
export enum UserType {
/**
* User with a server level login.
*/
WithLogin = 'WithLogin',
/**
* User based on a Windows user/group that has no login, but can connect to the Database Engine through membership in a Windows group.
*/
WithWindowsGroupLogin = 'WithWindowsGroupLogin',
/**
* Contained user, authentication is done within the database.
*/
Contained = 'Contained',
/**
* User that cannot authenticate.
*/
NoConnectAccess = 'NoConnectAccess'
}
/**
* The authentication types.
*/
export enum AuthenticationType {
Windows = 'Windows',
Sql = 'Sql',
AzureActiveDirectory = 'AAD'
}
export const CreateUserDocUrl = 'https://learn.microsoft.com/en-us/sql/t-sql/statements/create-user-transact-sql';
export const AlterUserDocUrl = 'https://learn.microsoft.com/en-us/sql/t-sql/statements/alter-user-transact-sql';
export const CreateLoginDocUrl = 'https://learn.microsoft.com/en-us/sql/t-sql/statements/create-login-transact-sql';
export const AlterLoginDocUrl = 'https://learn.microsoft.com/en-us/sql/t-sql/statements/alter-login-transact-sql';
export enum TelemetryActions {
CreateObject = 'CreateObject',
UpdateObject = 'UpdateObject',
DeleteObject = 'DeleteObject',
OpenNewObjectDialog = 'OpenNewObjectDialog',
OpenPropertiesDialog = 'OpenPropertiesDialog'
}
export enum TelemetryViews {
ObjectManagement = 'ObjectManagement'
}

View File

@@ -0,0 +1,132 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
// Object Types
export const LoginTypeDisplayName: string = localize('objectManagement.LoginTypeDisplayName', "login");
export const UserTypeDisplayName: string = localize('objectManagement.UserDisplayName', "user");
export const LoginTypeDisplayNameInTitle: string = localize('objectManagement.LoginTypeDisplayNameInTitle', "Login");
export const UserTypeDisplayNameInTitle: string = localize('objectManagement.UserTypeDisplayNameInTitle', "User");
// Shared Strings
export const HelpText: string = localize('objectManagement.helpText', "Help");
export const YesText: string = localize('objectManagement.yesText', "Yes");
export const OkText: string = localize('objectManagement.OkText', "OK");
export const LoadingDialogText: string = localize('objectManagement.loadingDialog', "Loading dialog...")
export function RefreshObjectExplorerError(error: string): string {
return localize({
key: 'objectManagement.refreshOEError',
comment: ['{0}: error message.']
}, "An error occurred while while refreshing the object explorer. {0}", error);
}
export function DeleteObjectConfirmationText(objectType: string, objectName: string): string {
return localize({
key: 'objectManagement.deleteObjectConfirmation',
comment: ['{0} object type, {1}: object name.']
}, "Are you sure you want to delete the {0}: {1}?", objectType, objectName);
}
export function CreateObjectOperationDisplayName(objectType: string): string {
return localize({
key: 'objectManagement.createObjectOperationName',
comment: ['{0} object type']
}, "Create {0}", objectType);
}
export function UpdateObjectOperationDisplayName(objectType: string, objectName: string): string {
return localize({
key: 'objectManagement.updateObjectOperationName',
comment: ['{0} object type, {1}: object name.']
}, "Update {0} '{1}'", objectType, objectName);
}
export function DeleteObjectOperationDisplayName(objectType: string, objectName: string): string {
return localize({
key: 'objectManagement.deleteObjectOperationName',
comment: ['{0} object type, {1}: object name.']
}, "Delete {0} '{1}'", objectType, objectName);
}
export function DeleteObjectError(objectType: string, objectName: string, error: string): string {
return localize({
key: 'objectManagement.deleteObjectError',
comment: ['{0} object type, {1}: object name, {2}: error message.']
}, "An error occurred while deleting the {0}: {1}. {2}", objectType, objectName, error);
}
export function OpenObjectPropertiesDialogError(objectType: string, objectName: string, error: string): string {
return localize({
key: 'objectManagement.openObjectPropertiesDialogError',
comment: ['{0} object type, {1}: object name, {2}: error message.']
}, "An error occurred while opening the properties dialog for {0}: {1}. {2}", objectType, objectName, error);
}
export function OpenNewObjectDialogError(objectType: string, error: string): string {
return localize({
key: 'objectManagement.openNewObjectDialogError',
comment: ['{0} object type, {1}: error message.']
}, "An error occurred while opening the new {0} dialog. {1}", objectType, error);
}
export function NewObjectDialogTitle(objectType: string): string {
return localize({
key: 'objectManagement.newObjectDialogTitle',
comment: ['{0} object type.']
}, '{0} - New (Preview)', objectType);
}
export function ObjectPropertiesDialogTitle(objectType: string, objectName: string): string {
return localize({
key: 'objectManagement.objectPropertiesDialogTitle',
comment: ['{0} object type, {1}: object name.']
}, '{0} - {1} (Preview)', objectType, objectName);
}
export const NameText = localize('objectManagement.nameLabel', "Name");
export const SelectedText = localize('objectManagement.selectedLabel', "Selected");
export const GeneralSectionHeader = localize('objectManagement.generalSectionHeader', "General");
export const AdvancedSectionHeader = localize('objectManagement.advancedSectionHeader', "Advanced");
export const PasswordText = localize('objectManagement.passwordLabel', "Password");
export const ConfirmPasswordText = localize('objectManagement.confirmPasswordLabel', "Confirm password");
export const EnabledText = localize('objectManagement.enabledLabel', "Enabled");
export const NameCannotBeEmptyError = localize('objectManagement.nameCannotBeEmptyError', "Name cannot be empty.");
export const PasswordCannotBeEmptyError = localize('objectManagement.passwordCannotBeEmptyError', "Password cannot be empty.");
export const PasswordsNotMatchError = localize('objectManagement.passwordsNotMatchError', "Password must match the confirm password.");
export const InvalidPasswordError = localize('objectManagement.invalidPasswordError', "Password doesn't meet the complexity requirement. For more information: https://docs.microsoft.com/sql/relational-databases/security/password-policy");
// Login
export const BlankPasswordConfirmationText: string = localize('objectManagement.blankPasswordConfirmation', "Creating a login with a blank password is a security risk. Are you sure you want to continue?");
export const DeleteLoginConfirmationText: string = localize('objectManagement.deleteLoginConfirmation', "Deleting server logins does not delete the database users associated with the logins. To complete the process, delete the users in each database. It may be necessary to first transfer the ownership of schemas to new users.");
export const SQLAuthenticationSectionHeader = localize('objectManagement.login.sqlAuthSectionHeader', "SQL Authentication");
export const ServerRoleSectionHeader = localize('objectManagement.login.serverRoleSectionHeader', "Server Roles");
export const AuthTypeText = localize('objectManagement.login.authenticateType', "Authentication");
export const SpecifyOldPasswordText = localize('objectManagement.login.specifyOldPasswordLabel', "Specify old password");
export const OldPasswordText = localize('objectManagement.login.oldPasswordLabel', "Old password");
export const EnforcePasswordPolicyText = localize('objectManagement.login.enforcePasswordPolicyLabel', "Enforce password policy");
export const EnforcePasswordExpirationText = localize('objectManagement.login.enforcePasswordExpirationLabel', "Enforce password expiration");
export const MustChangePasswordText = localize('objectManagement.login.mustChangePasswordLabel', "User must change password at next login");
export const DefaultDatabaseText = localize('objectManagement.login.defaultDatabaseLabel', "Default database");
export const DefaultLanguageText = localize('objectManagement.login.defaultLanguageLabel', "Default language");
export const PermissionToConnectText = localize('objectManagement.login.permissionToConnectLabel', "Permission to connect to database engine");
export const LoginLockedOutText = localize('objectManagement.login.lockedOutLabel', "Login is locked out");
export const WindowsAuthenticationTypeDisplayText = localize('objectManagement.login.windowsAuthenticationType', "Windows Authentication");
export const SQLAuthenticationTypeDisplayText = localize('objectManagement.login.sqlAuthenticationType', "SQL Authentication");
export const AADAuthenticationTypeDisplayText = localize('objectManagement.login.aadAuthenticationType', "Azure Active Directory Authentication");
export const OldPasswordCannotBeEmptyError = localize('objectManagement.login.oldPasswordCannotBeEmptyError', "Old password cannot be empty.");
// User
export const UserTypeText = localize('objectManagement.user.type', "Type");
export const UserWithLoginText = localize('objectManagement.user.userWithLogin', "User with login");
export const UserWithWindowsGroupLoginText = localize('objectManagement.user.userWithGroupLogin', "User with Windows group login");
export const ContainedUserText = localize('objectManagement.user.containedUser', "Contained user");
export const UserWithNoConnectAccess = localize('objectManagement.user.userWithNoConnectAccess', "User with no connect access");
export const DefaultSchemaText = localize('objectManagement.user.defaultSchemaLabel', "Default schema");
export const LoginText = localize('objectManagement.user.loginLabel', "Login");
export const OwnedSchemaSectionHeader = localize('objectManagement.user.ownedSchemasLabel', "Owned Schemas");
export const MembershipSectionHeader = localize('objectManagement.user.membershipLabel', "Membership");

View File

@@ -0,0 +1,303 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ISqlOpsFeature, SqlOpsDataClient } from 'dataprotocol-client';
import { ObjectManagement, IObjectManagementService } from 'mssql';
import { ClientCapabilities } from 'vscode-languageclient';
import { AppContext } from '../appContext';
import * as Utils from '../utils';
import * as constants from '../constants';
import * as contracts from '../contracts';
import { AuthenticationType, UserType } from './constants';
export class ObjectManagementService implements IObjectManagementService {
public static asFeature(context: AppContext): ISqlOpsFeature {
return class extends ObjectManagementService {
constructor(client: SqlOpsDataClient) {
super(context, client);
}
fillClientCapabilities(capabilities: ClientCapabilities): void {
Utils.ensure(capabilities, 'objectManagement')!.objectManagement = true;
}
initialize(): void {
}
};
}
private constructor(context: AppContext, protected readonly client: SqlOpsDataClient) {
context.registerService(constants.ObjectManagementService, this);
}
initializeLoginView(connectionUri: string, contextId: string, isNewObject: boolean, name: string | undefined): Thenable<ObjectManagement.LoginViewInfo> {
const params: contracts.InitializeLoginViewRequestParams = { connectionUri, contextId, isNewObject, name };
return this.client.sendRequest(contracts.InitializeLoginViewRequest.type, params).then(
r => {
return r;
},
e => {
this.client.logFailedRequest(contracts.InitializeLoginViewRequest.type, e);
return Promise.reject(new Error(e.message));
}
);
}
createLogin(contextId: string, login: ObjectManagement.Login): Thenable<void> {
const params: contracts.CreateLoginRequestParams = { contextId, login };
return this.client.sendRequest(contracts.CreateLoginRequest.type, params).then(
r => { },
e => {
this.client.logFailedRequest(contracts.CreateLoginRequest.type, e);
return Promise.reject(new Error(e.message));
}
);
}
updateLogin(contextId: string, login: ObjectManagement.Login): Thenable<void> {
const params: contracts.UpdateLoginRequestParams = { contextId, login };
return this.client.sendRequest(contracts.UpdateLoginRequest.type, params).then(
r => { },
e => {
this.client.logFailedRequest(contracts.UpdateLoginRequest.type, e);
return Promise.reject(new Error(e.message));
}
);
}
deleteLogin(connectionUri: string, name: string): Thenable<void> {
const params: contracts.DeleteLoginRequestParams = { connectionUri, name };
return this.client.sendRequest(contracts.DeleteLoginRequest.type, params).then(
r => { },
e => {
this.client.logFailedRequest(contracts.DeleteLoginRequest.type, e);
return Promise.reject(new Error(e.message));
}
);
}
disposeLoginView(contextId: string): Thenable<void> {
const params: contracts.DisposeLoginViewRequestParams = { contextId };
return this.client.sendRequest(contracts.DisposeLoginViewRequest.type, params).then(
r => { },
e => {
this.client.logFailedRequest(contracts.DisposeLoginViewRequest.type, e);
return Promise.reject(new Error(e.message));
}
);
}
initializeUserView(connectionUri: string, database: string, contextId: string, isNewObject: boolean, name: string | undefined): Thenable<ObjectManagement.UserViewInfo> {
const params: contracts.InitializeUserViewRequestParams = { connectionUri, database, contextId, isNewObject, name };
return this.client.sendRequest(contracts.InitializeUserViewRequest.type, params).then(
r => {
return r;
},
e => {
this.client.logFailedRequest(contracts.InitializeUserViewRequest.type, e);
return Promise.reject(new Error(e.message));
}
);
}
createUser(contextId: string, user: ObjectManagement.User): Thenable<void> {
const params: contracts.CreateUserRequestParams = { contextId, user };
return this.client.sendRequest(contracts.CreateUserRequest.type, params).then(
r => { },
e => {
this.client.logFailedRequest(contracts.CreateUserRequest.type, e);
return Promise.reject(new Error(e.message));
}
);
}
updateUser(contextId: string, user: ObjectManagement.User): Thenable<void> {
const params: contracts.UpdateUserRequestParams = { contextId, user };
return this.client.sendRequest(contracts.UpdateUserRequest.type, params).then(
r => { },
e => {
this.client.logFailedRequest(contracts.UpdateLoginRequest.type, e);
return Promise.reject(new Error(e.message));
}
);
}
deleteUser(connectionUri: string, database: string, name: string): Thenable<void> {
const params: contracts.DeleteUserRequestParams = { connectionUri, database, name };
return this.client.sendRequest(contracts.DeleteUserRequest.type, params).then(
r => { },
e => {
this.client.logFailedRequest(contracts.DeleteUserRequest.type, e);
return Promise.reject(new Error(e.message));
}
);
}
disposeUserView(contextId: string): Thenable<void> {
const params: contracts.DisposeUserViewRequestParams = { contextId };
return this.client.sendRequest(contracts.DisposeUserViewRequest.type, params).then(
r => { },
e => {
this.client.logFailedRequest(contracts.DisposeUserViewRequest.type, e);
return Promise.reject(new Error(e.message));
}
);
}
}
export class TestObjectManagementService implements IObjectManagementService {
initializeLoginView(connectionUri: string, contextId: string, isNewObject: boolean, name: string | undefined): Promise<ObjectManagement.LoginViewInfo> {
return new Promise((resolve, reject) => {
setTimeout(() => {
const serverRoles = ['sysadmin', 'public', 'bulkadmin', 'dbcreator', 'diskadmin', 'processadmin', 'securityadmin', 'serveradmin'];
const languages = ['<default>', 'English'];
const databases = ['master', 'db1', 'db2'];
let login: ObjectManagement.LoginViewInfo;
if (isNewObject) {
login = <ObjectManagement.LoginViewInfo>{
objectInfo: {
name: '',
authenticationType: AuthenticationType.Sql,
enforcePasswordPolicy: true,
enforcePasswordExpiration: true,
mustChangePassword: true,
defaultDatabase: 'master',
defaultLanguage: '<default>',
serverRoles: ['public', 'bulkadmin'],
connectPermission: true,
isEnabled: true,
isLockedOut: false
},
supportAADAuthentication: true,
supportSQLAuthentication: true,
supportWindowsAuthentication: true,
supportAdvancedOptions: true,
supportAdvancedPasswordOptions: true,
canEditLockedOutState: false,
languages: languages,
databases: databases,
serverRoles: serverRoles
};
} else {
login = <ObjectManagement.LoginViewInfo>{
objectInfo: {
name: name,
authenticationType: AuthenticationType.Sql,
enforcePasswordPolicy: true,
enforcePasswordExpiration: true,
mustChangePassword: true,
defaultDatabase: 'master',
defaultLanguage: '<default>',
serverRoles: ['public'],
connectPermission: true,
isEnabled: true,
isLockedOut: false,
password: '******************'
},
supportAADAuthentication: true,
supportSQLAuthentication: true,
supportWindowsAuthentication: true,
supportAdvancedOptions: true,
supportAdvancedPasswordOptions: true,
canEditLockedOutState: false,
languages: languages,
databases: databases,
serverRoles: serverRoles
};
}
resolve(login);
}, 3000);
});
}
async createLogin(contextId: string, login: ObjectManagement.Login): Promise<void> {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 3000);
});
}
async updateLogin(contextId: string, login: ObjectManagement.Login): Promise<void> {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 3000);
});
}
async deleteLogin(connectionUri: string, name: string): Promise<void> {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 3000);
});
}
async disposeLoginView(contextId: string): Promise<void> {
}
async initializeUserView(connectionUri: string, database: string, contextId: string, isNewObject: boolean, name: string): Promise<ObjectManagement.UserViewInfo> {
return new Promise((resolve, reject) => {
setTimeout(() => {
let viewInfo: ObjectManagement.UserViewInfo;
const languages = ['<default>', 'English'];
const schemas = ['dbo', 'sys', 'alanren'];
const logins = ['sa', 'alanren', 'alanren@microsoft.com'];
const databaseRoles = ['dbmanager', 'loginmanager', 'bulkadmin', 'sysadmin', 'tablemanager', 'viewmanager'];
if (isNewObject) {
viewInfo = {
objectInfo: <ObjectManagement.User>{
name: '',
type: UserType.WithLogin,
defaultSchema: 'dbo',
defaultLanguage: '<default>',
authenticationType: AuthenticationType.Sql,
loginName: 'sa',
ownedSchemas: [],
databaseRoles: []
},
languages: languages,
schemas: schemas,
logins: logins,
databaseRoles: databaseRoles,
supportContainedUser: true,
supportAADAuthentication: true,
supportSQLAuthentication: true,
supportWindowsAuthentication: true
};
} else {
viewInfo = {
objectInfo: <ObjectManagement.User>{
name: name,
type: UserType.WithLogin,
defaultSchema: 'dbo',
defaultLanguage: '<default>',
loginName: 'sa',
authenticationType: AuthenticationType.Sql,
ownedSchemas: ['dbo'],
databaseRoles: ['dbmanager', 'bulkadmin']
},
languages: languages,
schemas: schemas,
logins: logins,
databaseRoles: databaseRoles,
supportContainedUser: true,
supportAADAuthentication: true,
supportSQLAuthentication: true,
supportWindowsAuthentication: true
};
}
resolve(viewInfo);
}, 3000);
});
}
async createUser(contextId: string, user: ObjectManagement.User): Promise<void> {
return this.delayAndResolve();
}
async updateUser(contextId: string, login: ObjectManagement.User): Promise<void> {
return this.delayAndResolve();
}
async deleteUser(connectionUri: string, database: string, name: string): Promise<void> {
return this.delayAndResolve();
}
async disposeUserView(contextId: string): Promise<void> {
}
private delayAndResolve(): Promise<void> {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 3000);
});
}
}

View File

@@ -0,0 +1,299 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { DefaultInputWidth, ObjectManagementDialogBase } from './objectManagementDialogBase';
import { IObjectManagementService, ObjectManagement } from 'mssql';
import * as localizedConstants from '../localizedConstants';
import { AlterLoginDocUrl, AuthenticationType, CreateLoginDocUrl, NodeType, PublicServerRoleName } from '../constants';
import { getAuthenticationTypeByDisplayName, getAuthenticationTypeDisplayName, isValidSQLPassword } from '../utils';
export class LoginDialog extends ObjectManagementDialogBase<ObjectManagement.Login, ObjectManagement.LoginViewInfo> {
private formContainer: azdata.DivContainer;
private generalSection: azdata.GroupContainer;
private sqlAuthSection: azdata.GroupContainer;
private serverRoleSection: azdata.GroupContainer;
private advancedSection: azdata.GroupContainer;
private nameInput: azdata.InputBoxComponent;
private authTypeDropdown: azdata.DropDownComponent;
private passwordInput: azdata.InputBoxComponent;
private confirmPasswordInput: azdata.InputBoxComponent;
private specifyOldPasswordCheckbox: azdata.CheckBoxComponent;
private oldPasswordInput: azdata.InputBoxComponent;
private enforcePasswordPolicyCheckbox: azdata.CheckBoxComponent;
private enforcePasswordExpirationCheckbox: azdata.CheckBoxComponent;
private mustChangePasswordCheckbox: azdata.CheckBoxComponent;
private defaultDatabaseDropdown: azdata.DropDownComponent;
private defaultLanguageDropdown: azdata.DropDownComponent;
private serverRoleTable: azdata.TableComponent;
private connectPermissionCheckbox: azdata.CheckBoxComponent;
private enabledCheckbox: azdata.CheckBoxComponent;
private lockedOutCheckbox: azdata.CheckBoxComponent;
constructor(objectManagementService: IObjectManagementService, connectionUri: string, isNewObject: boolean, name?: string, objectExplorerContext?: azdata.ObjectExplorerContext) {
super(NodeType.Login, isNewObject ? CreateLoginDocUrl : AlterLoginDocUrl, objectManagementService, connectionUri, isNewObject, name, objectExplorerContext);
}
protected override async onConfirmation(): Promise<boolean> {
// Empty password is only allowed when advanced password options are supported and the password policy check is off.
// To match the SSMS behavior, a warning is shown to the user.
if (this.viewInfo.supportAdvancedPasswordOptions
&& this.objectInfo.authenticationType === AuthenticationType.Sql
&& !this.objectInfo.password
&& !this.objectInfo.enforcePasswordPolicy) {
const result = await vscode.window.showWarningMessage(localizedConstants.BlankPasswordConfirmationText, { modal: true }, localizedConstants.YesText);
return result === localizedConstants.YesText;
}
return true;
}
protected async validateInput(): Promise<string[]> {
const errors: string[] = [];
if (!this.objectInfo.name) {
errors.push(localizedConstants.NameCannotBeEmptyError);
}
if (this.objectInfo.authenticationType === AuthenticationType.Sql) {
if (!this.objectInfo.password && !(this.viewInfo.supportAdvancedPasswordOptions && !this.objectInfo.enforcePasswordPolicy)) {
errors.push(localizedConstants.PasswordCannotBeEmptyError);
}
if (this.objectInfo.password && (this.objectInfo.enforcePasswordPolicy || !this.viewInfo.supportAdvancedPasswordOptions)
&& !isValidSQLPassword(this.objectInfo.password, this.objectInfo.name)
&& (this.isNewObject || this.objectInfo.password !== this.originalObjectInfo.password)) {
errors.push(localizedConstants.InvalidPasswordError);
}
if (this.objectInfo.password !== this.confirmPasswordInput.value) {
errors.push(localizedConstants.PasswordsNotMatchError);
}
if (this.specifyOldPasswordCheckbox?.checked && !this.objectInfo.oldPassword) {
errors.push(localizedConstants.OldPasswordCannotBeEmptyError);
}
}
return errors;
}
protected async onComplete(): Promise<void> {
if (this.isNewObject) {
await this.objectManagementService.createLogin(this.contextId, this.objectInfo);
} else {
await this.objectManagementService.updateLogin(this.contextId, this.objectInfo);
}
}
protected async onDispose(): Promise<void> {
await this.objectManagementService.disposeLoginView(this.contextId);
}
protected async initializeData(): Promise<ObjectManagement.LoginViewInfo> {
const viewInfo = await this.objectManagementService.initializeLoginView(this.connectionUri, this.contextId, this.isNewObject, this.objectName);
viewInfo.objectInfo.password = viewInfo.objectInfo.password ?? '';
return viewInfo;
}
protected async initializeUI(): Promise<void> {
this.dialogObject.registerContent(async view => {
const sections: azdata.Component[] = [];
this.initializeGeneralSection(view);
sections.push(this.generalSection);
if (this.isNewObject || this.objectInfo.authenticationType === 'Sql') {
this.initializeSqlAuthSection(view);
sections.push(this.sqlAuthSection);
}
this.initializeServerRolesSection(view);
sections.push(this.serverRoleSection);
if (this.viewInfo.supportAdvancedOptions) {
this.initializeAdvancedSection(view);
sections.push(this.advancedSection);
}
this.formContainer = this.createFormContainer(view, sections);
return view.initializeModel(this.formContainer)
});
}
private initializeGeneralSection(view: azdata.ModelView): void {
this.nameInput = view.modelBuilder.inputBox().withProps({
ariaLabel: localizedConstants.NameText,
enabled: this.isNewObject,
value: this.objectInfo.name,
width: DefaultInputWidth
}).component();
this.nameInput.onTextChanged(async () => {
this.objectInfo.name = this.nameInput.value;
this.onObjectValueChange();
await this.runValidation(false);
});
const nameContainer = this.createLabelInputContainer(view, localizedConstants.NameText, this.nameInput);
const authTypes = [];
if (this.viewInfo.supportWindowsAuthentication) {
authTypes.push(localizedConstants.WindowsAuthenticationTypeDisplayText);
}
if (this.viewInfo.supportSQLAuthentication) {
authTypes.push(localizedConstants.SQLAuthenticationTypeDisplayText);
}
if (this.viewInfo.supportAADAuthentication) {
authTypes.push(localizedConstants.AADAuthenticationTypeDisplayText);
}
this.authTypeDropdown = view.modelBuilder.dropDown().withProps({
ariaLabel: localizedConstants.AuthTypeText,
values: authTypes,
value: getAuthenticationTypeDisplayName(this.objectInfo.authenticationType),
width: DefaultInputWidth,
enabled: this.isNewObject
}).component();
this.authTypeDropdown.onValueChanged(async () => {
this.objectInfo.authenticationType = getAuthenticationTypeByDisplayName(<string>this.authTypeDropdown.value);
this.setViewByAuthenticationType();
this.onObjectValueChange();
await this.runValidation(false);
});
const authTypeContainer = this.createLabelInputContainer(view, localizedConstants.AuthTypeText, this.authTypeDropdown);
this.enabledCheckbox = this.createCheckbox(view, localizedConstants.EnabledText, this.objectInfo.isEnabled);
this.enabledCheckbox.onChanged(() => {
this.objectInfo.isEnabled = this.enabledCheckbox.checked;
this.onObjectValueChange();
});
this.generalSection = this.createGroup(view, localizedConstants.GeneralSectionHeader, [nameContainer, authTypeContainer, this.enabledCheckbox], false);
}
private initializeSqlAuthSection(view: azdata.ModelView): void {
const items: azdata.Component[] = [];
this.passwordInput = this.createPasswordInputBox(view, localizedConstants.PasswordText, this.objectInfo.password ?? '');
const passwordRow = this.createLabelInputContainer(view, localizedConstants.PasswordText, this.passwordInput);
this.confirmPasswordInput = this.createPasswordInputBox(view, localizedConstants.ConfirmPasswordText, this.objectInfo.password ?? '');
this.passwordInput.onTextChanged(async () => {
this.objectInfo.password = this.passwordInput.value;
this.onObjectValueChange();
await this.runValidation(false);
});
this.confirmPasswordInput.onTextChanged(async () => {
await this.runValidation(false);
});
const confirmPasswordRow = this.createLabelInputContainer(view, localizedConstants.ConfirmPasswordText, this.confirmPasswordInput);
items.push(passwordRow, confirmPasswordRow);
if (!this.isNewObject) {
this.specifyOldPasswordCheckbox = this.createCheckbox(view, localizedConstants.SpecifyOldPasswordText);
this.oldPasswordInput = this.createPasswordInputBox(view, localizedConstants.OldPasswordText, '', false);
const oldPasswordRow = this.createLabelInputContainer(view, localizedConstants.OldPasswordText, this.oldPasswordInput);
this.specifyOldPasswordCheckbox.onChanged(async () => {
this.oldPasswordInput.enabled = this.specifyOldPasswordCheckbox.checked;
this.objectInfo.oldPassword = '';
if (!this.specifyOldPasswordCheckbox.checked) {
this.oldPasswordInput.value = '';
}
this.onObjectValueChange();
await this.runValidation(false);
});
this.oldPasswordInput.onTextChanged(async () => {
this.objectInfo.oldPassword = this.oldPasswordInput.value;
this.onObjectValueChange();
await this.runValidation(false);
});
items.push(this.specifyOldPasswordCheckbox, oldPasswordRow);
}
if (this.viewInfo.supportAdvancedPasswordOptions) {
this.enforcePasswordPolicyCheckbox = this.createCheckbox(view, localizedConstants.EnforcePasswordPolicyText, this.objectInfo.enforcePasswordPolicy);
this.enforcePasswordExpirationCheckbox = this.createCheckbox(view, localizedConstants.EnforcePasswordExpirationText, this.objectInfo.enforcePasswordPolicy);
this.mustChangePasswordCheckbox = this.createCheckbox(view, localizedConstants.MustChangePasswordText, this.objectInfo.mustChangePassword);
this.enforcePasswordPolicyCheckbox.onChanged(async () => {
const enforcePolicy = this.enforcePasswordPolicyCheckbox.checked;
this.objectInfo.enforcePasswordPolicy = enforcePolicy;
this.enforcePasswordExpirationCheckbox.enabled = enforcePolicy;
this.mustChangePasswordCheckbox.enabled = enforcePolicy;
this.enforcePasswordExpirationCheckbox.checked = enforcePolicy;
this.mustChangePasswordCheckbox.checked = enforcePolicy;
this.onObjectValueChange();
await this.runValidation(false);
});
this.enforcePasswordExpirationCheckbox.onChanged(() => {
const enforceExpiration = this.enforcePasswordExpirationCheckbox.checked;
this.objectInfo.enforcePasswordExpiration = enforceExpiration;
this.mustChangePasswordCheckbox.enabled = enforceExpiration;
this.mustChangePasswordCheckbox.checked = enforceExpiration;
this.onObjectValueChange();
});
this.mustChangePasswordCheckbox.onChanged(() => {
this.objectInfo.mustChangePassword = this.mustChangePasswordCheckbox.checked;
this.onObjectValueChange();
});
items.push(this.enforcePasswordPolicyCheckbox, this.enforcePasswordExpirationCheckbox, this.mustChangePasswordCheckbox);
if (!this.isNewObject) {
this.lockedOutCheckbox = this.createCheckbox(view, localizedConstants.LoginLockedOutText, this.objectInfo.isLockedOut, this.viewInfo.canEditLockedOutState);
items.push(this.lockedOutCheckbox);
this.lockedOutCheckbox.onChanged(() => {
this.objectInfo.isLockedOut = this.lockedOutCheckbox.checked;
this.onObjectValueChange();
});
}
}
this.sqlAuthSection = this.createGroup(view, localizedConstants.SQLAuthenticationSectionHeader, items);
}
private initializeAdvancedSection(view: azdata.ModelView): void {
const items: azdata.Component[] = [];
if (this.viewInfo.supportAdvancedOptions) {
this.defaultDatabaseDropdown = view.modelBuilder.dropDown().withProps({
ariaLabel: localizedConstants.DefaultDatabaseText,
values: this.viewInfo.databases,
value: this.objectInfo.defaultDatabase,
width: DefaultInputWidth
}).component();
const defaultDatabaseContainer = this.createLabelInputContainer(view, localizedConstants.DefaultDatabaseText, this.defaultDatabaseDropdown);
this.defaultDatabaseDropdown.onValueChanged(() => {
this.objectInfo.defaultDatabase = <string>this.defaultDatabaseDropdown.value;
this.onObjectValueChange();
});
this.defaultLanguageDropdown = view.modelBuilder.dropDown().withProps({
ariaLabel: localizedConstants.DefaultLanguageText,
values: this.viewInfo.languages,
value: this.objectInfo.defaultLanguage,
width: DefaultInputWidth
}).component();
const defaultLanguageContainer = this.createLabelInputContainer(view, localizedConstants.DefaultLanguageText, this.defaultLanguageDropdown);
this.defaultLanguageDropdown.onValueChanged(() => {
this.objectInfo.defaultLanguage = <string>this.defaultLanguageDropdown.value;
this.onObjectValueChange();
});
this.connectPermissionCheckbox = this.createCheckbox(view, localizedConstants.PermissionToConnectText, this.objectInfo.connectPermission);
this.connectPermissionCheckbox.onChanged(() => {
this.objectInfo.connectPermission = this.connectPermissionCheckbox.checked;
this.onObjectValueChange();
});
items.push(defaultDatabaseContainer, defaultLanguageContainer, this.connectPermissionCheckbox);
}
this.advancedSection = this.createGroup(view, localizedConstants.AdvancedSectionHeader, items);
}
private initializeServerRolesSection(view: azdata.ModelView): void {
const serverRolesData = this.viewInfo.serverRoles.map(name => {
const isRoleSelected = this.objectInfo.serverRoles.indexOf(name) !== -1;
const isRoleSelectionEnabled = name !== PublicServerRoleName;
return [{ enabled: isRoleSelectionEnabled, checked: isRoleSelected }, name];
});
this.serverRoleTable = this.createTableList(view, localizedConstants.ServerRoleSectionHeader, this.viewInfo.serverRoles, this.objectInfo.serverRoles, serverRolesData);
this.serverRoleSection = this.createGroup(view, localizedConstants.ServerRoleSectionHeader, [this.serverRoleTable]);
}
private setViewByAuthenticationType(): void {
if (this.authTypeDropdown.value === localizedConstants.SQLAuthenticationTypeDisplayText) {
this.addItem(this.formContainer, this.sqlAuthSection, 1);
} else if (this.authTypeDropdown.value !== localizedConstants.SQLAuthenticationTypeDisplayText) {
this.removeItem(this.formContainer, this.sqlAuthSection);
}
}
}

View File

@@ -0,0 +1,260 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// TODO:
// 1. include server properties and other properties in the telemetry.
import * as azdata from 'azdata';
import { IObjectManagementService, ObjectManagement } from 'mssql';
import * as vscode from 'vscode';
import { EOL } from 'os';
import { generateUuid } from 'vscode-languageclient/lib/utils/uuid';
import { getErrorMessage } from '../../utils';
import { NodeType, TelemetryActions, TelemetryViews } from '../constants';
import {
CreateObjectOperationDisplayName, HelpText, LoadingDialogText,
NameText,
NewObjectDialogTitle, ObjectPropertiesDialogTitle, OkText, SelectedText, UpdateObjectOperationDisplayName
} from '../localizedConstants';
import { deepClone, getNodeTypeDisplayName, refreshNode } from '../utils';
import { TelemetryReporter } from '../../telemetry';
export const DefaultLabelWidth = 150;
export const DefaultInputWidth = 300;
export const DefaultTableWidth = DefaultInputWidth + DefaultLabelWidth;
export const DefaultTableMaxHeight = 400;
export const DefaultTableMinRowCount = 2;
export const TableRowHeight = 25;
export const TableColumnHeaderHeight = 30;
export function getTableHeight(rowCount: number, minRowCount: number = DefaultTableMinRowCount, maxHeight: number = DefaultTableMaxHeight): number {
return Math.min(Math.max(rowCount, minRowCount) * TableRowHeight + TableColumnHeaderHeight, maxHeight);
}
function getDialogName(type: NodeType, isNewObject: boolean): string {
return isNewObject ? `New${type}` : `${type}Properties`
}
export abstract class ObjectManagementDialogBase<ObjectInfoType extends ObjectManagement.SqlObject, ViewInfoType extends ObjectManagement.ObjectViewInfo<ObjectInfoType>> {
protected readonly disposables: vscode.Disposable[] = [];
protected readonly dialogObject: azdata.window.Dialog;
protected readonly contextId: string;
private _viewInfo: ViewInfoType;
private _originalObjectInfo: ObjectInfoType;
constructor(private readonly objectType: NodeType,
docUrl: string,
protected readonly objectManagementService: IObjectManagementService,
protected readonly connectionUri: string,
protected isNewObject: boolean,
protected readonly objectName: string | undefined = undefined,
protected readonly objectExplorerContext?: azdata.ObjectExplorerContext,
dialogWidth: azdata.window.DialogWidth = 'narrow') {
const objectTypeDisplayName = getNodeTypeDisplayName(objectType, true);
const dialogTitle = isNewObject ? NewObjectDialogTitle(objectTypeDisplayName) : ObjectPropertiesDialogTitle(objectTypeDisplayName, objectName);
this.dialogObject = azdata.window.createModelViewDialog(dialogTitle, getDialogName(objectType, isNewObject), dialogWidth);
this.dialogObject.okButton.label = OkText;
this.disposables.push(this.dialogObject.onClosed(async () => { await this.dispose(); }));
const helpButton = azdata.window.createButton(HelpText, 'left');
this.disposables.push(helpButton.onClick(async () => {
await vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(docUrl));
}));
this.dialogObject.customButtons = [helpButton];
this.contextId = generateUuid();
this.dialogObject.registerCloseValidator(async (): Promise<boolean> => {
const confirmed = await this.onConfirmation();
if (!confirmed) {
return false;
}
return await this.runValidation();
});
}
protected abstract initializeData(): Promise<ViewInfoType>;
protected abstract initializeUI(): Promise<void>;
protected abstract onComplete(): Promise<void>;
protected abstract onDispose(): Promise<void>;
protected abstract validateInput(): Promise<string[]>;
protected onObjectValueChange(): void {
this.dialogObject.okButton.enabled = JSON.stringify(this.objectInfo) !== JSON.stringify(this._originalObjectInfo);
}
protected async onConfirmation(): Promise<boolean> {
return true;
}
protected get viewInfo(): ViewInfoType {
return this._viewInfo;
}
protected get objectInfo(): ObjectInfoType {
return this._viewInfo?.objectInfo;
}
protected get originalObjectInfo(): ObjectInfoType {
return this._originalObjectInfo;
}
public async open(): Promise<void> {
await vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: LoadingDialogText
}, async () => {
try {
this._viewInfo = await this.initializeData();
this._originalObjectInfo = deepClone(this.objectInfo);
await this.initializeUI();
const typeDisplayName = getNodeTypeDisplayName(this.objectType);
this.dialogObject.registerOperation({
displayName: this.isNewObject ? CreateObjectOperationDisplayName(typeDisplayName)
: UpdateObjectOperationDisplayName(typeDisplayName, this.objectName),
description: '',
isCancelable: false,
operation: async (operation: azdata.BackgroundOperation): Promise<void> => {
const actionName = this.isNewObject ? TelemetryActions.CreateObject : TelemetryActions.UpdateObject;
try {
if (JSON.stringify(this.objectInfo) !== JSON.stringify(this._originalObjectInfo)) {
const startTime = Date.now();
await this.onComplete();
if (this.isNewObject && this.objectExplorerContext) {
await refreshNode(this.objectExplorerContext);
}
TelemetryReporter.sendTelemetryEvent(actionName, {
objectType: this.objectType
}, {
ellapsedTime: Date.now() - startTime
});
operation.updateStatus(azdata.TaskStatus.Succeeded);
}
}
catch (err) {
operation.updateStatus(azdata.TaskStatus.Failed, getErrorMessage(err));
TelemetryReporter.createErrorEvent(TelemetryViews.ObjectManagement, actionName).withAdditionalProperties({
objectType: this.objectType
}).send();
}
}
});
azdata.window.openDialog(this.dialogObject);
} catch (err) {
const actionName = this.isNewObject ? TelemetryActions.OpenNewObjectDialog : TelemetryActions.OpenPropertiesDialog;
TelemetryReporter.createErrorEvent(TelemetryViews.ObjectManagement, actionName).withAdditionalProperties({
objectType: this.objectType
}).send();
void vscode.window.showErrorMessage(getErrorMessage(err));
}
});
}
private async dispose(): Promise<void> {
await this.onDispose();
this.disposables.forEach(disposable => disposable.dispose());
}
protected async runValidation(showErrorMessage: boolean = true): Promise<boolean> {
const errors = await this.validateInput();
if (errors.length > 0 && (this.dialogObject.message || showErrorMessage)) {
this.dialogObject.message = {
text: errors.join(EOL),
level: azdata.window.MessageLevel.Error
};
} else {
this.dialogObject.message = undefined;
}
return errors.length === 0;
}
protected createLabelInputContainer(view: azdata.ModelView, label: string, input: azdata.InputBoxComponent | azdata.DropDownComponent): azdata.FlexContainer {
const labelComponent = view.modelBuilder.text().withProps({ width: DefaultLabelWidth, value: label, requiredIndicator: input.required }).component();
const row = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'horizontal', flexWrap: 'nowrap', alignItems: 'center' }).withItems([labelComponent, input]).component();
return row;
}
protected createCheckbox(view: azdata.ModelView, label: string, checked: boolean = false, enabled: boolean = true): azdata.CheckBoxComponent {
return view.modelBuilder.checkBox().withProps({
label: label,
checked: checked,
enabled: enabled
}).component();
}
protected createPasswordInputBox(view: azdata.ModelView, ariaLabel: string, value: string = '', enabled: boolean = true, width: number = DefaultInputWidth): azdata.InputBoxComponent {
return this.createInputBox(view, ariaLabel, value, enabled, 'password', width);
}
protected createInputBox(view: azdata.ModelView, ariaLabel: string, value: string = '', enabled: boolean = true, type: azdata.InputBoxInputType = 'text', width: number = DefaultInputWidth): azdata.InputBoxComponent {
return view.modelBuilder.inputBox().withProps({ inputType: type, enabled: enabled, ariaLabel: ariaLabel, value: value, width: width }).component();
}
protected createGroup(view: azdata.ModelView, header: string, items: azdata.Component[], collapsible: boolean = true, collapsed: boolean = false): azdata.GroupContainer {
return view.modelBuilder.groupContainer().withLayout({
header: header,
collapsed: false,
collapsible: collapsible
}).withProps({ collapsed: collapsed }).withItems(items).component();
}
protected createFormContainer(view: azdata.ModelView, items: azdata.Component[]): azdata.DivContainer {
return view.modelBuilder.divContainer().withLayout({ width: 'calc(100% - 20px)', height: 'calc(100% - 20px)' }).withProps({
CSSStyles: { 'padding': '10px' }
}).withItems(items, { CSSStyles: { 'margin-block-end': '10px' } }).component();
}
protected createTableList(view: azdata.ModelView, ariaLabel: string, listValues: string[], selectedValues: string[], data?: any[][]): azdata.TableComponent {
let tableData = data;
if (tableData === undefined) {
tableData = listValues.map(name => {
const isSelected = selectedValues.indexOf(name) !== -1;
return [isSelected, name];
});
}
const table = view.modelBuilder.table().withProps(
{
ariaLabel: ariaLabel,
data: tableData,
columns: [
{
value: SelectedText,
type: azdata.ColumnType.checkBox,
options: { actionOnCheckbox: azdata.ActionOnCellCheckboxCheck.customAction }
}, {
value: NameText,
}
],
width: DefaultTableWidth,
height: getTableHeight(tableData.length)
}
).component();
table.onCellAction((arg: azdata.ICheckboxCellActionEventArgs) => {
const name = listValues[arg.row];
const idx = selectedValues.indexOf(name);
if (arg.checked && idx === -1) {
selectedValues.push(name);
} else if (!arg.checked && idx !== -1) {
selectedValues.splice(idx, 1)
}
this.onObjectValueChange();
});
return table;
}
protected removeItem(container: azdata.DivContainer | azdata.FlexContainer, item: azdata.Component): void {
if (container.items.indexOf(item) !== -1) {
container.removeItem(item);
}
}
protected addItem(container: azdata.DivContainer | azdata.FlexContainer, item: azdata.Component, index?: number): void {
if (container.items.indexOf(item) === -1) {
if (index === undefined) {
container.addItem(item);
} else {
container.insertItem(item, index);
}
}
}
}

View File

@@ -0,0 +1,248 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import { DefaultInputWidth, ObjectManagementDialogBase } from './objectManagementDialogBase';
import { IObjectManagementService, ObjectManagement } from 'mssql';
import * as localizedConstants from '../localizedConstants';
import { AlterUserDocUrl, AuthenticationType, CreateUserDocUrl, NodeType, UserType } from '../constants';
import { getAuthenticationTypeByDisplayName, getAuthenticationTypeDisplayName, getUserTypeByDisplayName, getUserTypeDisplayName, isValidSQLPassword } from '../utils';
export class UserDialog extends ObjectManagementDialogBase<ObjectManagement.User, ObjectManagement.UserViewInfo> {
private formContainer: azdata.DivContainer;
private generalSection: azdata.GroupContainer;
private ownedSchemaSection: azdata.GroupContainer;
private membershipSection: azdata.GroupContainer;
private advancedSection: azdata.GroupContainer;
private nameInput: azdata.InputBoxComponent;
private typeDropdown: azdata.DropDownComponent;
private typeContainer: azdata.FlexContainer;
private authTypeDropdown: azdata.DropDownComponent;
private authTypeContainer: azdata.FlexContainer;
private loginDropdown: azdata.DropDownComponent;
private loginContainer: azdata.FlexContainer;
private passwordInput: azdata.InputBoxComponent;
private passwordContainer: azdata.FlexContainer;
private confirmPasswordInput: azdata.InputBoxComponent;
private confirmPasswordContainer: azdata.FlexContainer;
private defaultSchemaDropdown: azdata.DropDownComponent;
private defaultSchemaContainer: azdata.FlexContainer;
private defaultLanguageDropdown: azdata.DropDownComponent;
private ownedSchemaTable: azdata.TableComponent;
private membershipTable: azdata.TableComponent;
constructor(objectManagementService: IObjectManagementService, connectionUri: string, private readonly database: string, isNewObject: boolean, name?: string, objectExplorerContext?: azdata.ObjectExplorerContext) {
super(NodeType.User, isNewObject ? CreateUserDocUrl : AlterUserDocUrl, objectManagementService, connectionUri, isNewObject, name, objectExplorerContext);
}
protected async initializeData(): Promise<ObjectManagement.UserViewInfo> {
const viewInfo = await this.objectManagementService.initializeUserView(this.connectionUri, this.database, this.contextId, this.isNewObject, this.objectName);
viewInfo.objectInfo.password = viewInfo.objectInfo.password ?? '';
return viewInfo;
}
protected async validateInput(): Promise<string[]> {
const errors: string[] = [];
if (!this.objectInfo.name) {
errors.push(localizedConstants.NameCannotBeEmptyError);
}
if (this.objectInfo.type === UserType.Contained && this.objectInfo.authenticationType === AuthenticationType.Sql) {
if (!this.objectInfo.password) {
errors.push(localizedConstants.PasswordCannotBeEmptyError);
}
if (this.objectInfo.password !== this.confirmPasswordInput.value) {
errors.push(localizedConstants.PasswordsNotMatchError);
}
if (!isValidSQLPassword(this.objectInfo.password, this.objectInfo.name)
&& (this.isNewObject || this.objectInfo.password !== this.originalObjectInfo.password)) {
errors.push(localizedConstants.InvalidPasswordError);
}
}
return errors;
}
protected async onComplete(): Promise<void> {
if (this.isNewObject) {
await this.objectManagementService.createUser(this.contextId, this.objectInfo);
} else {
await this.objectManagementService.updateUser(this.contextId, this.objectInfo);
}
}
protected async onDispose(): Promise<void> {
await this.objectManagementService.disposeUserView(this.contextId);
}
protected async initializeUI(): Promise<void> {
this.dialogObject.registerContent(async view => {
const sections: azdata.Component[] = [];
this.initializeGeneralSection(view);
this.initializeOwnedSchemaSection(view);
this.initializeMembershipSection(view);
this.initializeAdvancedSection(view);
sections.push(this.generalSection, this.ownedSchemaSection, this.membershipSection, this.advancedSection);
this.formContainer = this.createFormContainer(view, sections);
setTimeout(() => {
this.setViewByUserType();
}, 100);
return view.initializeModel(this.formContainer)
});
}
private initializeGeneralSection(view: azdata.ModelView): void {
this.nameInput = view.modelBuilder.inputBox().withProps({
ariaLabel: localizedConstants.NameText,
enabled: this.isNewObject,
value: this.objectInfo.name,
width: DefaultInputWidth
}).component();
this.nameInput.onTextChanged(async () => {
this.objectInfo.name = this.nameInput.value;
this.onObjectValueChange();
await this.runValidation(false);
});
const nameContainer = this.createLabelInputContainer(view, localizedConstants.NameText, this.nameInput);
this.defaultSchemaDropdown = view.modelBuilder.dropDown().withProps({
ariaLabel: localizedConstants.DefaultSchemaText,
values: this.viewInfo.schemas,
value: this.objectInfo.defaultSchema,
width: DefaultInputWidth
}).component();
this.defaultSchemaContainer = this.createLabelInputContainer(view, localizedConstants.DefaultSchemaText, this.defaultSchemaDropdown);
this.defaultSchemaDropdown.onValueChanged(() => {
this.objectInfo.defaultSchema = <string>this.defaultSchemaDropdown.value;
this.onObjectValueChange();
});
this.typeDropdown = view.modelBuilder.dropDown().withProps({
ariaLabel: localizedConstants.UserTypeText,
values: [localizedConstants.UserWithLoginText, localizedConstants.UserWithWindowsGroupLoginText, localizedConstants.ContainedUserText, localizedConstants.UserWithNoConnectAccess],
value: getUserTypeDisplayName(this.objectInfo.type),
width: DefaultInputWidth,
enabled: this.isNewObject
}).component();
this.typeDropdown.onValueChanged(async () => {
this.objectInfo.type = getUserTypeByDisplayName(<string>this.typeDropdown.value);
this.onObjectValueChange();
this.setViewByUserType();
await this.runValidation(false);
});
this.typeContainer = this.createLabelInputContainer(view, localizedConstants.UserTypeText, this.typeDropdown);
this.loginDropdown = view.modelBuilder.dropDown().withProps({
ariaLabel: localizedConstants.LoginText,
values: this.viewInfo.logins,
value: this.objectInfo.loginName,
width: DefaultInputWidth,
enabled: this.isNewObject
}).component();
this.loginDropdown.onValueChanged(() => {
this.objectInfo.loginName = <string>this.loginDropdown.value;
this.onObjectValueChange();
});
this.loginContainer = this.createLabelInputContainer(view, localizedConstants.LoginText, this.loginDropdown);
const authTypes = [];
if (this.viewInfo.supportWindowsAuthentication) {
authTypes.push(localizedConstants.WindowsAuthenticationTypeDisplayText);
}
if (this.viewInfo.supportSQLAuthentication) {
authTypes.push(localizedConstants.SQLAuthenticationTypeDisplayText);
}
if (this.viewInfo.supportAADAuthentication) {
authTypes.push(localizedConstants.AADAuthenticationTypeDisplayText);
}
this.authTypeDropdown = view.modelBuilder.dropDown().withProps({
ariaLabel: localizedConstants.AuthTypeText,
values: authTypes,
value: getAuthenticationTypeDisplayName(this.objectInfo.authenticationType),
width: DefaultInputWidth,
enabled: this.isNewObject
}).component();
this.authTypeContainer = this.createLabelInputContainer(view, localizedConstants.AuthTypeText, this.authTypeDropdown);
this.authTypeDropdown.onValueChanged(async () => {
this.objectInfo.authenticationType = getAuthenticationTypeByDisplayName(<string>this.authTypeDropdown.value);
this.onObjectValueChange();
this.setViewByAuthenticationType();
await this.runValidation(false);
});
this.passwordInput = this.createPasswordInputBox(view, localizedConstants.PasswordText, this.objectInfo.password ?? '');
this.passwordContainer = this.createLabelInputContainer(view, localizedConstants.PasswordText, this.passwordInput);
this.confirmPasswordInput = this.createPasswordInputBox(view, localizedConstants.ConfirmPasswordText, this.objectInfo.password ?? '');
this.confirmPasswordContainer = this.createLabelInputContainer(view, localizedConstants.ConfirmPasswordText, this.confirmPasswordInput);
this.passwordInput.onTextChanged(async () => {
this.objectInfo.password = this.passwordInput.value;
this.onObjectValueChange();
await this.runValidation(false);
});
this.confirmPasswordInput.onTextChanged(async () => {
await this.runValidation(false);
});
this.generalSection = this.createGroup(view, localizedConstants.GeneralSectionHeader, [
nameContainer,
this.defaultSchemaContainer,
this.typeContainer,
this.loginContainer,
this.authTypeContainer,
this.passwordContainer,
this.confirmPasswordContainer
], false);
}
private initializeOwnedSchemaSection(view: azdata.ModelView): void {
this.ownedSchemaTable = this.createTableList(view, localizedConstants.OwnedSchemaSectionHeader, this.viewInfo.schemas, this.objectInfo.ownedSchemas);
this.ownedSchemaSection = this.createGroup(view, localizedConstants.OwnedSchemaSectionHeader, [this.ownedSchemaTable]);
}
private initializeMembershipSection(view: azdata.ModelView): void {
this.membershipTable = this.createTableList(view, localizedConstants.MembershipSectionHeader, this.viewInfo.databaseRoles, this.objectInfo.databaseRoles);
this.membershipSection = this.createGroup(view, localizedConstants.MembershipSectionHeader, [this.membershipTable]);
}
private initializeAdvancedSection(view: azdata.ModelView): void {
this.defaultLanguageDropdown = view.modelBuilder.dropDown().withProps({
ariaLabel: localizedConstants.DefaultLanguageText,
values: this.viewInfo.languages,
value: this.objectInfo.defaultLanguage,
width: DefaultInputWidth
}).component();
this.defaultLanguageDropdown.onValueChanged(() => {
this.objectInfo.defaultLanguage = <string>this.defaultLanguageDropdown.value;
this.onObjectValueChange();
});
const container = this.createLabelInputContainer(view, localizedConstants.DefaultLanguageText, this.defaultLanguageDropdown);
this.advancedSection = this.createGroup(view, localizedConstants.AdvancedSectionHeader, [container]);
}
private setViewByUserType(): void {
if (this.typeDropdown.value === localizedConstants.UserWithLoginText) {
this.removeItem(this.generalSection, this.authTypeContainer);
this.removeItem(this.formContainer, this.advancedSection);
this.addItem(this.generalSection, this.loginContainer);
} else if (this.typeDropdown.value === localizedConstants.ContainedUserText) {
this.removeItem(this.generalSection, this.loginContainer);
this.addItem(this.generalSection, this.authTypeContainer);
this.addItem(this.formContainer, this.advancedSection);
} else {
this.removeItem(this.generalSection, this.loginContainer);
this.removeItem(this.generalSection, this.authTypeContainer);
this.removeItem(this.formContainer, this.advancedSection);
}
this.setViewByAuthenticationType();
}
private setViewByAuthenticationType(): void {
const showPassword = this.typeDropdown.value === localizedConstants.ContainedUserText && this.authTypeDropdown.value === localizedConstants.SQLAuthenticationTypeDisplayText;
if (showPassword) {
this.addItem(this.generalSection, this.passwordContainer);
this.addItem(this.generalSection, this.confirmPasswordContainer);
} else {
this.removeItem(this.generalSection, this.passwordContainer);
this.removeItem(this.generalSection, this.confirmPasswordContainer);
}
}
}

View File

@@ -0,0 +1,122 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { getErrorMessage } from '../utils';
import { AuthenticationType, NodeType, UserType } from './constants';
import { AADAuthenticationTypeDisplayText, ContainedUserText, LoginTypeDisplayName, LoginTypeDisplayNameInTitle, RefreshObjectExplorerError, SQLAuthenticationTypeDisplayText, UserTypeDisplayName, UserTypeDisplayNameInTitle, UserWithLoginText, UserWithNoConnectAccess, UserWithWindowsGroupLoginText, WindowsAuthenticationTypeDisplayText } from './localizedConstants';
export function deepClone<T>(obj: T): T {
if (!obj || typeof obj !== 'object') {
return obj;
}
if (obj instanceof RegExp) {
// See https://github.com/Microsoft/TypeScript/issues/10990
return obj as any;
}
const result: any = Array.isArray(obj) ? [] : {};
Object.keys(<any>obj).forEach((key: string) => {
if ((<any>obj)[key] && typeof (<any>obj)[key] === 'object') {
result[key] = deepClone((<any>obj)[key]);
} else {
result[key] = (<any>obj)[key];
}
});
return result;
}
export async function refreshParentNode(context: azdata.ObjectExplorerContext): Promise<void> {
if (context) {
try {
const node = await azdata.objectexplorer.getNode(context.connectionProfile.id, context.nodeInfo.nodePath);
const parentNode = await node?.getParent();
await parentNode?.refresh();
}
catch (err) {
await vscode.window.showErrorMessage(RefreshObjectExplorerError(getErrorMessage(err)));
}
}
}
export async function refreshNode(context: azdata.ObjectExplorerContext): Promise<void> {
if (context) {
try {
const node = await azdata.objectexplorer.getNode(context.connectionProfile.id, context.nodeInfo.nodePath);
await node?.refresh();
}
catch (err) {
await vscode.window.showErrorMessage(RefreshObjectExplorerError(getErrorMessage(err)));
}
}
}
export function getNodeTypeDisplayName(type: string, inTitle: boolean = false): string {
switch (type) {
case NodeType.Login:
return inTitle ? LoginTypeDisplayNameInTitle : LoginTypeDisplayName;
case NodeType.User:
return inTitle ? UserTypeDisplayNameInTitle : UserTypeDisplayName;
default:
throw new Error(`Unkown node type: ${type}`);
}
}
export function getAuthenticationTypeDisplayName(authType: AuthenticationType): string {
switch (authType) {
case AuthenticationType.Windows:
return WindowsAuthenticationTypeDisplayText;
case AuthenticationType.AzureActiveDirectory:
return AADAuthenticationTypeDisplayText;
default:
return SQLAuthenticationTypeDisplayText;
}
}
export function getAuthenticationTypeByDisplayName(displayValue: string): AuthenticationType {
switch (displayValue) {
case WindowsAuthenticationTypeDisplayText:
return AuthenticationType.Windows;
case AADAuthenticationTypeDisplayText:
return AuthenticationType.AzureActiveDirectory;
default:
return AuthenticationType.Sql;
}
}
export function getUserTypeDisplayName(userType: UserType): string {
switch (userType) {
case UserType.WithLogin:
return UserWithLoginText;
case UserType.WithWindowsGroupLogin:
return UserWithWindowsGroupLoginText;
case UserType.Contained:
return ContainedUserText;
default:
return UserWithNoConnectAccess;
}
}
export function getUserTypeByDisplayName(userTypeDisplayName: string): UserType {
switch (userTypeDisplayName) {
case UserWithLoginText:
return UserType.WithLogin;
case UserWithWindowsGroupLoginText:
return UserType.WithWindowsGroupLogin;
case ContainedUserText:
return UserType.Contained;
default:
return UserType.NoConnectAccess;
}
}
// https://docs.microsoft.com/sql/relational-databases/security/password-policy
export function isValidSQLPassword(password: string, userName: string = 'sa'): boolean {
const containsUserName = password && userName !== undefined && password.toUpperCase().includes(userName.toUpperCase());
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);
}