diff --git a/src/Microsoft.SqlTools.ServiceLayer/Security/UserActions.cs b/src/Microsoft.SqlTools.ServiceLayer/Security/UserActions.cs index 4014270e..cf949631 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Security/UserActions.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Security/UserActions.cs @@ -60,6 +60,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security /// internal async Task HandleInitializeUserViewRequest(InitializeUserViewParams parameters, RequestContext requestContext) { + // check input parameters if (string.IsNullOrWhiteSpace(parameters.Database)) { throw new ArgumentNullException("parameters.Database"); @@ -70,13 +71,13 @@ namespace Microsoft.SqlTools.ServiceLayer.Security throw new ArgumentNullException("parameters.ContextId"); } + // open a connection for running the user dialog and associated task ConnectionInfo originalConnInfo; ConnectionServiceInstance.TryFindConnection(parameters.ConnectionUri, out originalConnInfo); if (originalConnInfo == null) { throw new ArgumentException("Invalid connection URI '{0}'", parameters.ConnectionUri); } - string originalDatabaseName = originalConnInfo.ConnectionDetails.DatabaseName; try { @@ -93,26 +94,22 @@ namespace Microsoft.SqlTools.ServiceLayer.Security { originalConnInfo.ConnectionDetails.DatabaseName = originalDatabaseName; } - ConnectionInfo connInfo; this.ConnectionServiceInstance.TryFindConnection(parameters.ContextId, out connInfo); - CDataContainer dataContainer = CreateUserDataContainer(connInfo, null, ConfigAction.Create, parameters.Database); + // create a default user data context and database object + CDataContainer dataContainer = CreateUserDataContainer(connInfo, null, ConfigAction.Create, parameters.Database); + string databaseUrn = string.Format(System.Globalization.CultureInfo.InvariantCulture, + "Server/Database[@Name='{0}']", Urn.EscapeString(parameters.Database)); + Database? parentDb = dataContainer.Server.GetSmoObject(databaseUrn) as Database; + + // if viewing an exisitng user then populate some properties UserInfo? userInfo = null; + ExhaustiveUserTypes userType = ExhaustiveUserTypes.LoginMappedUser; if (!parameters.IsNewObject) { - User? existingUser = null; - string databaseUrn = string.Format(System.Globalization.CultureInfo.InvariantCulture, - "Server/Database[@Name='{0}']", - Urn.EscapeString(parameters.Database)); - Database? parentDb = dataContainer.Server.GetSmoObject(databaseUrn) as Database; - existingUser = dataContainer.Server.Databases[parentDb.Name].Users[parameters.Name]; - - if (string.IsNullOrWhiteSpace(existingUser.Login)) - { - throw new ApplicationException("Only 'User with Login' user type supported"); - } - + User? existingUser = dataContainer.Server.Databases[parentDb.Name].Users[parameters.Name]; + userType = UserActions.GetCurrentUserTypeForExistingUser(existingUser); userInfo = new UserInfo() { Name = parameters.Name, @@ -121,9 +118,10 @@ namespace Microsoft.SqlTools.ServiceLayer.Security }; } - UserPrototypeFactory userPrototypeFactory = UserPrototypeFactory.GetInstance(dataContainer, userInfo, originalData: null); - UserPrototype currentUserPrototype = userPrototypeFactory.GetUserPrototype(ExhaustiveUserTypes.LoginMappedUser); + // generate a user prototype + UserPrototype currentUserPrototype = UserPrototypeFactory.GetUserPrototype(dataContainer, userInfo, originalData: null, userType); + // get the default language if available IUserPrototypeWithDefaultLanguage defaultLanguagePrototype = currentUserPrototype as IUserPrototypeWithDefaultLanguage; string? defaultLanguageAlias = null; if (defaultLanguagePrototype != null && defaultLanguagePrototype.IsDefaultLanguageSupported) @@ -140,12 +138,14 @@ namespace Microsoft.SqlTools.ServiceLayer.Security } } + // get the default schema if available string? defaultSchema = null; IUserPrototypeWithDefaultSchema defaultSchemaPrototype = currentUserPrototype as IUserPrototypeWithDefaultSchema; if (defaultSchemaPrototype != null && defaultSchemaPrototype.IsDefaultSchemaSupported) { defaultSchema = defaultSchemaPrototype.DefaultSchema; } + // IUserPrototypeWithPassword userWithPwdPrototype = currentUserPrototype as IUserPrototypeWithPassword; // if (userWithPwdPrototype != null && !this.DataContainer.IsNewObject) // { @@ -153,6 +153,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security // this.confirmPwdTextBox.Text = FAKE_PASSWORD; // } + // get the login name if it exists string? loginName = null; IUserPrototypeWithMappedLogin mappedLoginPrototype = currentUserPrototype as IUserPrototypeWithMappedLogin; if (mappedLoginPrototype != null) @@ -160,6 +161,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security loginName = mappedLoginPrototype.LoginName; } + // populate user's role assignments List databaseRoles = new List(); foreach (string role in currentUserPrototype.DatabaseRoleNames) { @@ -169,6 +171,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security } } + // populate user's schema ownerships List schemaNames = new List(); foreach (string schema in currentUserPrototype.SchemaNames) { @@ -178,12 +181,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Security } } - // default to dbo schema, if there isn't already a default - if (string.IsNullOrWhiteSpace(defaultSchema) && currentUserPrototype.SchemaNames.Contains("dbo")) - { - defaultSchema = "dbo"; - } - ServerConnection serverConnection = dataContainer.ServerConnection; UserViewInfo userViewInfo = new UserViewInfo() { @@ -198,7 +195,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security DatabaseRoles = databaseRoles.ToArray(), DefaultLanguage = defaultLanguageAlias }, - SupportContainedUser = false, // support for these will be added later + SupportContainedUser = UserActions.IsParentDatabaseContained(parentDb), // support for these will be added later SupportWindowsAuthentication = false, SupportAADAuthentication = false, SupportSQLAuthentication = true, @@ -349,7 +346,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security } CDataContainer dataContainer = CreateUserDataContainer(connInfo, user, configAction, databaseName); - using (var actions = new UserActions(dataContainer, user, configAction, originalData)) + using (var actions = new UserActions(dataContainer, configAction, user, originalData)) { var executionHandler = new ExecutonHandler(actions); executionHandler.RunNow(runType, this); @@ -379,8 +376,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Security /// public UserActions( CDataContainer context, - UserInfo? user, ConfigAction configAction, + UserInfo? user, UserPrototypeData? originalData) { this.DataContainer = context; @@ -401,19 +398,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Security #endregion /// - /// called on background thread by the framework to execute the action + /// called by the management actions framework to execute the action /// /// public override void OnRunNow(object sender) { - if (this.configAction == ConfigAction.Drop) - { - // if (this.credentialData.Credential != null) - // { - // this.credentialData.Credential.DropIfExists(); - // } - } - else + if (this.configAction != ConfigAction.Drop) { this.userPrototype.ApplyChanges(); } @@ -422,30 +412,42 @@ namespace Microsoft.SqlTools.ServiceLayer.Security private UserPrototype InitUserPrototype(CDataContainer dataContainer, UserInfo user, UserPrototypeData? originalData) { ExhaustiveUserTypes currentUserType; - UserPrototypeFactory userPrototypeFactory = UserPrototypeFactory.GetInstance(dataContainer, user, originalData); - if (dataContainer.IsNewObject) { - if (dataContainer.Server != null && IsParentDatabaseContained(dataContainer.ParentUrn, dataContainer.Server)) - { - currentUserType = ExhaustiveUserTypes.SqlUserWithPassword; - } - else - { - currentUserType = ExhaustiveUserTypes.LoginMappedUser; - } + currentUserType = GetUserTypeForUserInfo(user); } else { - currentUserType = this.GetCurrentUserTypeForExistingUser( + currentUserType = UserActions.GetCurrentUserTypeForExistingUser( dataContainer.Server.GetSmoObject(dataContainer.ObjectUrn) as User); } - UserPrototype currentUserPrototype = userPrototypeFactory.GetUserPrototype(currentUserType); - return currentUserPrototype; + UserPrototype currentUserPrototype = UserPrototypeFactory.GetUserPrototype(dataContainer, user, originalData, currentUserType); + return currentUserPrototype; } - private ExhaustiveUserTypes GetCurrentUserTypeForExistingUser(User? user) + private ExhaustiveUserTypes GetUserTypeForUserInfo(UserInfo user) + { + ExhaustiveUserTypes userType = ExhaustiveUserTypes.LoginMappedUser; + switch (user.Type) + { + case DatabaseUserType.WithLogin: + userType = ExhaustiveUserTypes.LoginMappedUser; + break; + case DatabaseUserType.WithWindowsGroupLogin: + userType = ExhaustiveUserTypes.WindowsUser; + break; + case DatabaseUserType.Contained: + userType = ExhaustiveUserTypes.SqlUserWithPassword; + break; + case DatabaseUserType.NoConnectAccess: + userType = ExhaustiveUserTypes.SqlUserWithoutLogin; + break; + } + return userType; + } + + internal static ExhaustiveUserTypes GetCurrentUserTypeForExistingUser(User? user) { if (user == null) { @@ -478,18 +480,16 @@ namespace Microsoft.SqlTools.ServiceLayer.Security } } - private static bool IsParentDatabaseContained(Urn parentDbUrn, Server server) + internal static bool IsParentDatabaseContained(Urn parentDbUrn, Server server) { string parentDbName = parentDbUrn.GetNameForType("Database"); - Database parentDatabase = server.Databases[parentDbName]; + return IsParentDatabaseContained(server.Databases[parentDbName]); + } - if (parentDatabase.IsSupportedProperty("ContainmentType") - && parentDatabase.ContainmentType == ContainmentType.Partial) - { - return true; - } - - return false; + internal static bool IsParentDatabaseContained(Database parentDatabase) + { + return parentDatabase.IsSupportedProperty("ContainmentType") + && parentDatabase.ContainmentType == ContainmentType.Partial; } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Security/UserData.cs b/src/Microsoft.SqlTools.ServiceLayer/Security/UserData.cs index f088cad9..65fd50ea 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Security/UserData.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Security/UserData.cs @@ -763,12 +763,14 @@ namespace Microsoft.SqlTools.ServiceLayer.Security { base.SaveProperties(user); - if (!this.Exists || (user.Login != this.currentState.mappedLoginName)) + bool isValidLoginName = !string.IsNullOrWhiteSpace(this.currentState.mappedLoginName); + bool isCreatingOrUpdatingLogin = !this.Exists || user.Login != this.currentState.mappedLoginName; + if (isValidLoginName && isCreatingOrUpdatingLogin) { user.Login = this.currentState.mappedLoginName; } } - } + } internal class UserPrototypeForWindowsUser : UserPrototypeForSqlUserWithLogin, IUserPrototypeWithDefaultLanguage @@ -985,107 +987,47 @@ namespace Microsoft.SqlTools.ServiceLayer.Security /// /// Used to create or return required UserPrototype objects for a user type. - /// This factory class also helps us in maintaining a single set of current data - /// and original data mapped to all UserPrototypes. - /// - /// Also this UserPrototypeFactory is a Singleton object for one datacontainer object. - /// Making it Singleton helps us in using same factory object inside other pages too. /// - internal class UserPrototypeFactory + internal static class UserPrototypeFactory { - private static UserPrototypeFactory? singletonInstance; - - private UserPrototypeData currentData; - private UserPrototypeData originalData; - private CDataContainer context; - - private UserPrototype? asymmetricKeyMappedUser; - private UserPrototype? certificateMappedUser; - private UserPrototype? loginMappedUser; - private UserPrototype? noLoginUser; - private UserPrototype? sqlUserWithPassword; - private UserPrototype? windowsUser; - - private UserPrototype? currentPrototype; - - public UserPrototype CurrentPrototype - { - get - { - currentPrototype ??= new UserPrototype(this.context, - this.currentData, - this.originalData); - return currentPrototype; - } - } - - private UserPrototypeFactory(CDataContainer context, UserInfo user, UserPrototypeData? originalData) - { - this.context = context; - - this.currentData = new UserPrototypeData(this.context, user); - this.originalData = originalData ?? this.currentData.Clone(); - } - - public static UserPrototypeFactory GetInstance(CDataContainer context, UserInfo? user, UserPrototypeData? originalData) - { - if (singletonInstance != null - && singletonInstance.context != context) - { - singletonInstance = null; - } - - singletonInstance ??= new UserPrototypeFactory(context, user, originalData); - - return singletonInstance; - } - - public UserPrototype GetUserPrototype(ExhaustiveUserTypes userType) + public static UserPrototype GetUserPrototype( + CDataContainer context, UserInfo? user, + UserPrototypeData? originalData, ExhaustiveUserTypes userType) { + UserPrototype currentPrototype = null; + UserPrototypeData currentData = new UserPrototypeData(context, user); switch (userType) { case ExhaustiveUserTypes.AsymmetricKeyMappedUser: - currentData.userType = UserType.AsymmetricKey; - this.asymmetricKeyMappedUser ??= new UserPrototype(this.context, this.currentData, this.originalData); - this.currentPrototype = asymmetricKeyMappedUser; + currentPrototype ??= new UserPrototype(context, currentData, originalData); break; case ExhaustiveUserTypes.CertificateMappedUser: - currentData.userType = UserType.Certificate; - this.certificateMappedUser ??= new UserPrototype(this.context, this.currentData, this.originalData); - this.currentPrototype = certificateMappedUser; + currentPrototype ??= new UserPrototype(context, currentData, originalData); break; case ExhaustiveUserTypes.LoginMappedUser: - currentData.userType = UserType.SqlUser; - this.loginMappedUser ??= new UserPrototypeForSqlUserWithLogin(this.context, this.currentData, this.originalData); - this.currentPrototype = loginMappedUser; + currentPrototype ??= new UserPrototypeForSqlUserWithLogin(context, currentData, originalData); break; case ExhaustiveUserTypes.SqlUserWithoutLogin: - currentData.userType = UserType.NoLogin; - this.noLoginUser ??= new UserPrototypeWithDefaultSchema(this.context, this.currentData, this.originalData); - this.currentPrototype = noLoginUser; + currentPrototype ??= new UserPrototypeWithDefaultSchema(context, currentData, originalData); break; case ExhaustiveUserTypes.SqlUserWithPassword: - currentData.userType = UserType.SqlUser; - this.sqlUserWithPassword ??= new UserPrototypeForSqlUserWithPassword(this.context, this.currentData, this.originalData); - this.currentPrototype = sqlUserWithPassword; + currentPrototype ??= new UserPrototypeForSqlUserWithPassword(context, currentData, originalData); break; case ExhaustiveUserTypes.WindowsUser: - currentData.userType = UserType.SqlUser; - this.windowsUser ??= new UserPrototypeForWindowsUser(this.context, this.currentData, this.originalData); - this.currentPrototype = windowsUser; + currentPrototype ??= new UserPrototypeForWindowsUser(context, currentData, originalData); break; default: System.Diagnostics.Debug.Assert(false, "Unknown UserType provided."); - this.currentPrototype = null; + currentPrototype = null; break; } - return this.currentPrototype; + return currentPrototype; } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Security/SecurityTestUtils.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Security/SecurityTestUtils.cs index b6090787..23bd36f7 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Security/SecurityTestUtils.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Security/SecurityTestUtils.cs @@ -61,12 +61,12 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Security }; } - internal static UserInfo GetTestUserInfo(string loginName) + internal static UserInfo GetTestUserInfo(DatabaseUserType userType, string userName = null, string loginName = null) { return new UserInfo() { - Type = DatabaseUserType.WithLogin, - Name = "TestUserName_" + new Random().NextInt64(10000000, 90000000).ToString(), + Type = userType, + Name = userName ?? "TestUserName_" + new Random().NextInt64(10000000, 90000000).ToString(), LoginName = loginName, Password = "placeholder", DefaultSchema = "dbo", @@ -161,7 +161,9 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Security internal static async Task CreateUser( UserServiceHandlerImpl service, TestConnectionResult connectionResult, - LoginInfo login) + DatabaseUserType userType, + string userName = null, + string loginName = null) { string contextId = System.Guid.NewGuid().ToString(); var initializeViewRequestParams = new InitializeUserViewParams @@ -181,7 +183,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Security var userParams = new CreateUserParams { ContextId = contextId, - User = SecurityTestUtils.GetTestUserInfo(login.Name) + User = SecurityTestUtils.GetTestUserInfo(userType, userName, loginName) }; var createUserContext = new Mock>(); diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Security/UserTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Security/UserTests.cs index 8427ae07..e3997b23 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Security/UserTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Security/UserTests.cs @@ -3,9 +3,11 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using System; using System.Threading.Tasks; using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; using Microsoft.SqlTools.ServiceLayer.Security; +using Microsoft.SqlTools.ServiceLayer.Security.Contracts; using Microsoft.SqlTools.ServiceLayer.Test.Common; using NUnit.Framework; @@ -31,7 +33,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Security var login = await SecurityTestUtils.CreateLogin(service, connectionResult); - var user = await SecurityTestUtils.CreateUser(userService, connectionResult, login); + var user = await SecurityTestUtils.CreateUser(userService, connectionResult, DatabaseUserType.WithLogin, null, login.Name); await SecurityTestUtils.DropObject(connectionResult.ConnectionInfo.OwnerUri, SecurityTestUtils.GetUserURN(connectionResult.ConnectionInfo.ConnectionDetails.DatabaseName, user.Name)); @@ -39,6 +41,29 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Security } } + /// + /// Test the basic Create User method handler + /// + // [Test] - Windows-only + public async Task TestHandleCreateUserWithWindowsGroup() + { + using (SelfCleaningTempFile queryTempFile = new SelfCleaningTempFile()) + { + // setup + SecurityService service = new SecurityService(); + UserServiceHandlerImpl userService = new UserServiceHandlerImpl(); + var connectionResult = await LiveConnectionHelper.InitLiveConnectionInfoAsync("master", queryTempFile.FilePath); + + var user = await SecurityTestUtils.CreateUser( + userService, + connectionResult, + DatabaseUserType.WithWindowsGroupLogin, + $"{Environment.MachineName}\\Administrator"); + + await SecurityTestUtils.DropObject(connectionResult.ConnectionInfo.OwnerUri, SecurityTestUtils.GetUserURN(connectionResult.ConnectionInfo.ConnectionDetails.DatabaseName, user.Name)); + } + } + /// /// Test the basic Update User method handler /// @@ -54,7 +79,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Security var login = await SecurityTestUtils.CreateLogin(service, connectionResult); - var user = await SecurityTestUtils.CreateUser(userService, connectionResult, login); + var user = await SecurityTestUtils.CreateUser(userService, connectionResult, DatabaseUserType.WithLogin, null, login.Name); await SecurityTestUtils.UpdateUser(userService, connectionResult, user);