From 3c25549986495061c6b8ab3adc24e3f359cd3ea2 Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Tue, 31 Jan 2023 21:12:53 -0800 Subject: [PATCH] Initial WIP code for user management (#1838) * Initial user management code * WIP * Fix whitespace * WIP user objects * WIP user objects * Cleanup ported code * WIP * WIP * Update the User contracts * Additional cleanups * Remove warning silencing which isn't intended for this PR * Fix some warnings as error in CI --- .../Management/Common/Utils.cs | 70 + .../Security/Contracts/LoginInfo.cs | 58 + .../Security/Contracts/LoginRequest.cs | 66 + .../Security/Contracts/UserInfo.cs | 124 + .../Security/CreateLoginData.cs | 2473 +++++++++++++++++ .../Security/SecurityService.cs | 728 +++++ .../SqlCollationSensitiveStringComparer.cs | 80 + .../Security/UserData.cs | 1128 ++++++++ .../Security/LoginTests.cs | 69 + .../Security/SecurityTestUtils.cs | 22 + ...oft.SqlTools.ServiceLayer.UnitTests.csproj | 2 +- 11 files changed, 4819 insertions(+), 1 deletion(-) create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/LoginInfo.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/LoginRequest.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/UserInfo.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Security/CreateLoginData.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Security/SqlCollationSensitiveStringComparer.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Security/UserData.cs create mode 100644 test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Security/LoginTests.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/Management/Common/Utils.cs b/src/Microsoft.SqlTools.ServiceLayer/Management/Common/Utils.cs index ab2f5c5c..1962bfc5 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Management/Common/Utils.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Management/Common/Utils.cs @@ -287,6 +287,76 @@ namespace Microsoft.SqlTools.ServiceLayer.Management { return !string.IsNullOrEmpty(serverName) && serverName.StartsWith("asazure://", StringComparison.OrdinalIgnoreCase); } + + public static bool IsSql11OrLater(ServerVersion version) + { + return IsSql11OrLater(version.Major); + } + + public static bool IsSql11OrLater(int versionMajor) + { + return (versionMajor >= 11); + } + + public static bool IsSql12OrLater(ServerVersion version) + { + return IsSql12OrLater(version.Major); + } + + public static bool IsSql12OrLater(int versionMajor) + { + return (versionMajor >= 12); + } + + public static bool IsSql13OrLater(ServerVersion version) + { + return IsSql13OrLater(version.Major); + } + + public static bool IsSql13OrLater(int versionMajor) + { + return (versionMajor >= 13); + } + + public static bool IsSql14OrLater(ServerVersion version) + { + return IsSql14OrLater(version.Major); + } + + public static bool IsSql14OrLater(int versionMajor) + { + return (versionMajor >= 14); + } + + public static bool IsSql15OrLater(ServerVersion version) + { + return IsSql15OrLater(version.Major); + } + + public static bool IsSql15OrLater(int versionMajor) + { + return (versionMajor >= 15); + } + + public static bool IsSql16OrLater(ServerVersion version) + { + return IsSql16OrLater(version.Major); + } + + public static bool IsSql16OrLater(int versionMajor) + { + return (versionMajor >= 16); + } + + public static bool IsYukonOrAbove(SqlServer.Management.Smo.Server server) + { + return server.Version.Major >= 9; + } + + public static bool IsBelowYukon(SqlServer.Management.Smo.Server server) + { + return server.Version.Major < 9; + } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/LoginInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/LoginInfo.cs new file mode 100644 index 00000000..35544599 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/LoginInfo.cs @@ -0,0 +1,58 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace Microsoft.SqlTools.ServiceLayer.Security.Contracts +{ + [JsonConverter(typeof(StringEnumConverter))] + public enum LoginType + { + [EnumMember(Value = "Windows")] + Windows, + [EnumMember(Value = "Sql")] + Sql, + [EnumMember(Value = "AAD")] + AzureActiveDirectory + } + + /// + /// a class for storing various login properties + /// + public class LoginInfo + { + public string LoginName { get; set; } + + public LoginType LoginType { get; set; } + + public string CertificateName { get; set; } + + public string AsymmetricKeyName { get; set; } + + public bool WindowsGrantAccess { get; set; } + + public bool MustChange { get; set; } + + public bool IsDisabled { get; set; } + + public bool IsLockedOut { get; set; } + + public bool EnforcePolicy { get; set; } + + public bool EnforceExpiration { get; set; } + + public bool WindowsAuthSupported { get; set; } + + public string Password { get; set; } + + public string OldPassword { get; set; } + + public string DefaultLanguage { get; set; } + + public string DefaultDatabase { get; set; } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/LoginRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/LoginRequest.cs new file mode 100644 index 00000000..6a2eb8db --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/LoginRequest.cs @@ -0,0 +1,66 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.Hosting.Protocol.Contracts; +using Microsoft.SqlTools.ServiceLayer.Utility; +using Microsoft.SqlTools.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.Security.Contracts +{ + /// + /// Create Login parameters + /// + public class CreateLoginParams : GeneralRequestDetails + { + public string OwnerUri { get; set; } + + public LoginInfo Login { get; set; } + } + + /// + /// Create Login result + /// + public class CreateLoginResult : ResultStatus + { + public LoginInfo Login { get; set; } + } + + + /// + /// Create Login request type + /// + public class CreateLoginRequest + { + /// + /// Request definition + /// + public static readonly + RequestType Type = + RequestType.Create("security/createlogin"); + } + + /// + /// Delete Login params + /// + public class DeleteLoginParams : GeneralRequestDetails + { + public string OwnerUri { get; set; } + + public string LoginName { get; set; } + } + + /// + /// Delete Login request type + /// + public class DeleteLoginRequest + { + /// + /// Request definition + /// + public static readonly + RequestType Type = + RequestType.Create("security/deletelogin"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/UserInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/UserInfo.cs new file mode 100644 index 00000000..24a95813 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/UserInfo.cs @@ -0,0 +1,124 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace Microsoft.SqlTools.ServiceLayer.Security.Contracts +{ + [JsonConverter(typeof(StringEnumConverter))] + public enum DatabaseUserType + { + [EnumMember(Value = "UserWithLogin")] + UserWithLogin, + [EnumMember(Value = "UserWithoutLogin")] + UserWithoutLogin + } + + public class ExtendedProperty + { + + public string Name { get; set; } + + public string Value { get; set; } + } + + public class SqlObject + { + public string Name { get; set; } + + public string Path { get; set; } + } + + public class Permission + { + public string Name { get; set; } + + public bool Grant { get; set; } + + public bool WithGrant { get; set; } + + public bool Deny { get; set; } + } + + public class SecurablePermissions + { + public SqlObject Securable { get; set; } + + public Permission[] Permissions { get; set; } + } + + /// + /// a class for storing various user properties + /// + public class UserInfo + { + DatabaseUserType? Type { get; set; } + + public string LoginName { get; set; } + + public string Password { get; set; } + + public string DefaultSchema { get; set; } + + public string[] OwnedSchemas { get; set; } + + public bool isEnabled { get; set; } + + public bool isAAD { get; set; } + + public ExtendedProperty[] ExtendedProperties { get; set; } + + public SecurablePermissions[] SecurablePermissions { get; set; } + } +} + + +#if false + +export interface ServerRole extends SqlObject { + owner: string | undefined; + securablePermissions: SecurablePermissions[]; + members: SqlObject[]; + memberships: SqlObject[]; + isFixedRole: boolean; +} + +export interface ServerLogin extends SqlObject { + type: LoginType; + password: string | undefined; + oldPassword: string | undefined; + enforcePasswordPolicy: boolean | undefined; + enforcePasswordExpiration: boolean | undefined; + defaultDatabase: string; + defaultLanguage: string; + serverRoles: string[]; + userMapping: ServerLoginDatabaseUserMapping[]; + isGroup: boolean; + isEnabled: boolean; + connectPermission: boolean; + isLockedOut: boolean; +} + + + +export interface ServerLoginDatabaseUserMapping { + database: string; + user: string; + defaultSchema: string; + databaseRoles: string[]; +} + +export interface DatabaseRole extends SqlObject { + owner: string | undefined; + password: string | undefined; + ownedSchemas: string[]; + securablePermissions: SecurablePermissions[] | undefined; + extendedProperties: ExtendedProperty[] | undefined; + isFixedRole: boolean; +} + +#endif \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/Security/CreateLoginData.cs b/src/Microsoft.SqlTools.ServiceLayer/Security/CreateLoginData.cs new file mode 100644 index 00000000..e6a39582 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Security/CreateLoginData.cs @@ -0,0 +1,2473 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + + +using System; +using System.Collections; +using System.Collections.Specialized; +using System.Data; + +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.Sdk.Sfc; +using Microsoft.SqlServer.Management.Smo; +using Microsoft.SqlTools.ServiceLayer.Management; +using Microsoft.SqlTools.ServiceLayer.Security.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.Security +{ + /// + /// Encapsulates database roles, access and default schema + /// + internal class DatabaseRoles : ICloneable + { + private Microsoft.SqlServer.Management.Smo.Server server; + + private string databaseName; + private string loginName; + private bool permit; + private bool isAccessible; + private string defaultSchema; + private string userName; + private HybridDictionary databaseRoles; + private StringCollection schemaNames = null; + private bool loginExists; + private bool roleMembershipChanged; + private bool guestStatus; + + private bool initializedDatabaseAccess; + private bool initializedRoleMembership; + + /// + /// The name of the database + /// + public string DatabaseName + { + get + { + return databaseName; + } + } + + /// + /// The default schema (user name) for the login in the database + /// + public string DefaultSchema + { + get + { + if (!this.initializedDatabaseAccess) + { + this.InitializeDatabaseAccess(); + } + + return defaultSchema; + } + + set + { + System.Diagnostics.Debug.Assert(this.initializedDatabaseAccess, "unexpected property set for unitialized DatabaseRoles"); + defaultSchema = value; + } + } + + /// + /// Whether the login has access to the database + /// + public bool PermitDatabaseAccess + { + get + { + if (!this.initializedDatabaseAccess) + { + this.InitializeDatabaseAccess(); + } + + return permit; + } + + set + { + System.Diagnostics.Debug.Assert(this.initializedDatabaseAccess, "unexpected property set for unitialized DatabaseRoles"); + permit = value; + } + } + + /// + /// Whether the database is accessible + /// + public bool DatabaseIsAccessible + { + get + { + if (!this.initializedDatabaseAccess) + { + this.InitializeDatabaseAccess(); + } + + return this.isAccessible; + } + } + + /// + /// Guest account enables or disabled in the database + /// + public bool GuestStatus + { + get + { + if (!this.initializedRoleMembership) + { + this.InitializeRoleMembership(); + } + return this.guestStatus; + + } + } + + /// + /// The names of all the database roles defined for the database + /// + public string[] DatabaseRoleNames + { + get + { + if (!this.initializedRoleMembership) + { + this.InitializeRoleMembership(); + } + + SortedList sortedRoles = new SortedList(databaseRoles, Comparer.Default); + string[] result = new string[sortedRoles.Count]; + + sortedRoles.Keys.CopyTo(result, 0); + + return result; + } + } + + /// + /// The names of the schemas in the database sorted alphabetically + /// + public StringCollection SchemaNames + { + get + { + if (this.schemaNames == null) + { + this.schemaNames = new StringCollection(); + Enumerator enumerator = new Enumerator(); + Urn urn = new Urn(String.Format(System.Globalization.CultureInfo.InvariantCulture, "Server/Database[@Name='{0}']/Schema", Urn.EscapeString(databaseName))); + string[] fields = new string[] { "Name"}; + OrderBy[] orderBy = new OrderBy[] { new OrderBy("Name", OrderBy.Direction.Asc)}; + Request request = new Request(urn, fields, orderBy); + DataTable enumeratorResults = enumerator.Process(this.server.ConnectionContext, request); + + System.Diagnostics.Debug.Assert(enumeratorResults.Rows.Count != 0, "couldn't enumerate schemas in the database"); + + for (int i = 0; i < enumeratorResults.Rows.Count; ++i) + { + this.schemaNames.Add(enumeratorResults.Rows[i]["Name"].ToString()); + } + } + + return this.schemaNames; + } + } + + /// + /// Has the user changed membership in any role? + /// + public bool RoleMembershipChanged + { + get + { + return this.roleMembershipChanged; + } + } + /// + /// Whether the login already exists + /// + private bool LoginExists + { + get + { + return this.loginExists; + } + } + + + /// + /// constructor + /// + private DatabaseRoles() + { + this.DefaultInitialize(); + } + + /// + /// Constructor + /// + /// The server we are working with + /// The name of the database for which we are encapsulating data + public DatabaseRoles(Microsoft.SqlServer.Management.Smo.Server server, string databaseName) + { + this.DefaultInitialize(); + + this.server = server; + this.databaseName = databaseName; + + } + + /// + /// Constructor + /// + /// The server we are working with + /// The name of the database for which we are encapsulating data + /// The name of the login we are modifying + public DatabaseRoles(Microsoft.SqlServer.Management.Smo.Server server, string databaseName, string loginName) + { + this.DefaultInitialize(); + + this.server = server; + this.loginName = loginName; + this.databaseName = databaseName; + this.loginExists = true; + + } + + + /// + /// Gets whether the associated login is a member of a particular role + /// + /// The name of the database role + /// True if the login is a member of the role, false otherwise + public bool IsMember(string databaseRoleName) + { + if (!this.initializedRoleMembership) + { + this.InitializeRoleMembership(); + } + + System.Diagnostics.Debug.Assert(databaseRoles.Contains(databaseRoleName), "databaseRoleName is not the name of a role in the database"); + + bool isPublic = (0 == String.Compare(databaseRoleName, "public", StringComparison.Ordinal)); + bool result = isPublic || (bool) databaseRoles[databaseRoleName]; + + return result; + } + + /// + /// Sets whether the associated login is a member of a particular role + /// + /// The name of the database role + /// Whether the login is a member of the role + public void SetMember(string databaseRoleName, bool isMember) + { + System.Diagnostics.Debug.Assert(databaseRoles.Contains(databaseRoleName), "databaseRoleName is not the name of a role in the database"); + + if (0 != String.Compare(databaseRoleName, "public", StringComparison.Ordinal)) + { + databaseRoles[databaseRoleName] = isMember; + this.roleMembershipChanged = true; + } + } + + /// + /// Create a clone of this DatabaseRoles object + /// + /// The clone DatabaseRoles + public object Clone() + { + DatabaseRoles result = new DatabaseRoles(); + + result.server = this.server; + result.loginName = this.loginName; + result.databaseName = this.databaseName; + result.loginExists = this.loginExists; + result.permit = this.permit; + result.isAccessible = this.isAccessible; + result.defaultSchema = this.defaultSchema; + result.userName = this.userName; + result.guestStatus = this.guestStatus; + + result.initializedDatabaseAccess = this.initializedDatabaseAccess; + result.initializedRoleMembership = this.initializedRoleMembership; + + foreach (string key in this.databaseRoles.Keys) + { + result.databaseRoles[key] = this.databaseRoles[key]; + } + + return result; + } + + /// + /// Determine whether the login has access to the database + /// + /// the name of the schema associated with the login in the database + /// whether the user has database access + /// True if there is a user associated with the login in the database, false otherwise + private bool GetDatabaseUserInfo(out string userName, out bool hasDBAccess, out string defaultSchema) + { + bool result = false; + + userName = String.Empty; + hasDBAccess = false; + defaultSchema = String.Empty; + + if (this.isAccessible) + { + try + { + Request request = new Request(); + + request.Urn = String.Format(System.Globalization.CultureInfo.InvariantCulture, + "Server/Database[@Name='{0}']/User[@Login='{1}']", + Urn.EscapeString(databaseName), + Urn.EscapeString(this.loginName)); + + if (server.Information.Version.Major >= 9) + { + request.Fields = new string[3] { "Name", "HasDBAccess", "DefaultSchema"}; + } + else + { + request.Fields = new string[2] { "Name", "HasDBAccess"}; + } + + DataTable users = new Enumerator().Process(server.ConnectionContext, request); + + if (0 != users.Rows.Count) + { + System.Diagnostics.Debug.Assert(1 == users.Rows.Count, "unexpected number of users for the the login"); + + result = true; + + userName = Convert.ToString (users.Rows[0][0], System.Globalization.CultureInfo.InvariantCulture); + hasDBAccess = Convert.ToBoolean(users.Rows[0][1], System.Globalization.CultureInfo.InvariantCulture); + if (server.Information.Version.Major >= 9) + { + defaultSchema = Convert.ToString(users.Rows[0][2], System.Globalization.CultureInfo.InvariantCulture); + } + else + { + defaultSchema = String.Empty; + } + } + } + catch (EnumeratorException ex) + { + System.Diagnostics.Trace.TraceError(ex.Message); + + // if we got an exception determining whether the user has access, + // then the database is effectively inaccessible + this.isAccessible = false; + } + } + + return result; + } + + /// + /// Determine whether a particular user is a member of a role + /// + /// The name of the role + /// The name of the user + /// True if the user is a member of the role, false otherwise + private bool DatabaseRoleContainsUser(string roleName, string userName) + { + bool result = false; + + if (this.isAccessible && (0 != userName.Length)) + { + Request request = new Request(); + + request.Urn = String.Format(System.Globalization.CultureInfo.InvariantCulture, + "Server/Database[@Name='{0}']/Role[@Name='{1}']/Member[@Name='{2}']", + Urn.EscapeString(databaseName), + Urn.EscapeString(roleName), + Urn.EscapeString(userName)); + + request.Fields = new string[1] { "Name"}; + + DataTable members = new Enumerator().Process(server.ConnectionContext, request); + + if (0 != members.Rows.Count) + { + System.Diagnostics.Debug.Assert(1 == members.Rows.Count, "unexpected number of members for the user name"); + result = true; + } + } + + return result; + } + + /// + /// Initialize member variables to default values + /// + private void DefaultInitialize() + { + this.server = null; + this.loginName = String.Empty; + this.databaseName = String.Empty; + this.loginExists = false; + this.permit = false; + this.isAccessible = false; + this.defaultSchema = String.Empty; + this.userName = String.Empty; + this.databaseRoles = new HybridDictionary(); + this.guestStatus = false; + + this.initializedDatabaseAccess = false; + this.initializedRoleMembership = false; + this.roleMembershipChanged = false; + } + + /// + /// Determines status of the guest account in the database + /// + /// + private void GetGuestStatus(Database database) + { + System.Diagnostics.Debug.Assert(database != null, "we need a valid database to determine guest access!"); + if (database != null) + { + User guest = database.Users["guest"]; + + // if guest doesn't exist, then guest doesn't have access. + this.guestStatus = (guest != null) ? guest.HasDBAccess : false; + } + } + + /// + /// Initialize database access information + /// + private void InitializeDatabaseAccess() + { + this.initializedDatabaseAccess = true; + + if ((server != null) && (databaseName.Length != 0)) + { + // determine whether the database is accessible to the user + try + { + DataTable dt = new Enumerator().Process(this.server.ConnectionContext, + new Request(new Urn(string.Format("Server/Database[@Name='{0}']", Urn.EscapeString(databaseName))), + new string[] { "Status" })); + if (dt != null && + dt.Rows.Count > 0 && + dt.Rows[0]["Status"] != DBNull.Value) + { + this.isAccessible = (Convert.ToInt32(dt.Rows[0]["Status"]) & (int)DatabaseStatus.Normal) != 0; + } + else + { + this.isAccessible = false; + } + } + catch (Exception) + { + // if we got an exception checking accessibility, the database + // is inaccessible to the user at the very least + this.isAccessible = false; + } + + if (this.isAccessible && (0 != this.loginName.Length)) + { + this.GetDatabaseUserInfo(out this.userName, out this.permit, out this.defaultSchema); + } + } + } + + /// + /// Initialize role membership information for the database + /// + private void InitializeRoleMembership() + { + this.initializedRoleMembership = true; + + if (this.DatabaseIsAccessible) + { + // Get user information + Database database = server.Databases[this.databaseName]; + + if (database != null) + { + GetGuestStatus(database); + } + + string userName = String.Empty; + bool hasAccess = false; + string defaultSchema = String.Empty; + bool userExists = this.GetDatabaseUserInfo(out userName, out hasAccess, out defaultSchema); + + + // get database role names + Request request = new Request(); + + request.Urn = String.Format(System.Globalization.CultureInfo.InvariantCulture,"Server/Database[@Name='{0}']/Role", Urn.EscapeString(databaseName)); + request.Fields = new string[1] { "Name"}; + + DataTable roles = new Enumerator().Process(server.ConnectionContext, request); + int roleCount = roles.Rows.Count; + + // determine which roles the user is a member of + for (int roleIndex = 0; roleIndex < roleCount; ++roleIndex) + { + string roleName = roles.Rows[roleIndex][0].ToString(); + bool isRoleMember = (userExists) ? this.DatabaseRoleContainsUser(roleName, userName) : false; + + this.databaseRoles.Add(roleName, isRoleMember); + } + } + } + + /// + /// gets/sets the name of the user for our login in this database + /// + /// + public string UserName + { + get + { + if (!this.initializedDatabaseAccess) + { + this.InitializeDatabaseAccess(); + } + + return userName; + } + set + { + userName = value; + } + } + } + + /// + /// Encapsulates server roles of which a particular login is a member + /// + internal class ServerRoles : ICloneable + { + private Microsoft.SqlServer.Management.Smo.Server server; + private string loginName; + private bool loginExists; + private HybridDictionary serverRoles; + + private bool initialized; + + /// + /// Simple description, isMember pair - used as the value in the serverRoles map + /// + private class ServerRoleInfo : ICloneable + { + public string roleDescription; + public bool isMember; + + public ServerRoleInfo(string roleDescription) + { + this.roleDescription = roleDescription; + this.isMember = false; + } + + public ServerRoleInfo(string roleDescription, bool isMember) + { + this.roleDescription = roleDescription; + this.isMember = isMember; + } + + public object Clone() + { + ServerRoleInfo result = new ServerRoleInfo(this.roleDescription, this.isMember); + return result; + } + } + + /// + /// constructor + /// + private ServerRoles() + { + this.server = null; + this.loginName = String.Empty; + this.loginExists = false; + this.serverRoles = new HybridDictionary(); + this.initialized = false; + } + + /// + /// constructor + /// + /// The server with which we are working + public ServerRoles(Microsoft.SqlServer.Management.Smo.Server server) + { + this.server = server; + this.loginName = String.Empty; + this.loginExists = false; + this.serverRoles = new HybridDictionary(); + this.initialized = false; + } + + /// + /// constructor + /// + /// The server with which we are working + /// The name of the login we are modifying + public ServerRoles(Microsoft.SqlServer.Management.Smo.Server server, string loginName) + { + System.Diagnostics.Debug.Assert(server.Logins[loginName] != null, "loginName does not refer to an actual login on the server"); + + this.server = server; + this.loginName = loginName; + this.loginExists = true; + this.serverRoles = new HybridDictionary(); + this.initialized = false; + } + + + /// + /// Get the names of the roles defined on the server + /// + /// Array of role names + public string[] ServerRoleNames + { + get + { + if (!this.initialized) + { + PopulateServerRoles(); + } + + SortedList sortedRoles = new SortedList(serverRoles, Comparer.Default); + string[] result = new string[sortedRoles.Count]; + + sortedRoles.Keys.CopyTo(result, 0); + + return result; + } + } + + /// + /// Get whether the associated login is a member of a particular role + /// + /// The name of the role for which we are checking membership + /// True if the login is a member of the role, false otherwise + public bool IsMember(string serverRoleName) + { + if (!this.initialized) + { + this.PopulateServerRoles(); + } + + bool result = false; + + if (serverRoleName == "public") + { + result = true; + } + else if (serverRoles.Contains(serverRoleName)) + { + result = ((ServerRoleInfo) serverRoles[serverRoleName]).isMember; + } + + return result; + } + + /// + /// Set whether the associated login is a member of a particular role + /// + /// The name of the role whose membership we wish to modify + /// True if the login should be a member of the role, false otherwise + public void SetMember(string serverRoleName, bool isMember) + { + System.Diagnostics.Debug.Assert(serverRoles.Contains(serverRoleName), "serverRoleName is not the name of a role in the server"); + + if (0 != String.Compare(serverRoleName, "public", StringComparison.Ordinal)) + { + ((ServerRoleInfo) serverRoles[serverRoleName]).isMember = isMember; + } + } + + /// + /// Get the role description for a particular role + /// + /// The name of the role for which we are getting a description + /// The role description + public string GetDescription(string serverRoleName) + { + System.Diagnostics.Debug.Assert(serverRoles.Contains(serverRoleName), "serverRoleName is not the name of a role in the server"); + return((ServerRoleInfo) serverRoles[serverRoleName]).roleDescription; + } + /// + /// Create a clone of this ServerRoles object + /// + /// The clone ServerRoles object + public object Clone() + { + ServerRoles result = new ServerRoles(); + + result.server = this.server; + result.loginName = this.loginName; + result.loginExists = this.loginExists; + result.initialized = this.initialized; + + foreach (string key in this.serverRoles.Keys) + { + ServerRoleInfo roleInfo = (ServerRoleInfo) this.serverRoles[key]; + result.serverRoles[key] = roleInfo.Clone(); + } + + return result; + } + + /// + /// Populate the server roles map + /// + private void PopulateServerRoles() + { + this.initialized = true; + serverRoles.Clear(); + + try + { + foreach (ServerRole role in server.Roles) + { + bool isRoleMember = false; + + if (this.loginExists) + { + StringCollection roleMembers = new StringCollection(); + roleMembers = role.EnumMemberNames(); + isRoleMember = roleMembers.Contains(this.loginName); + } + + string roleDescription = String.Empty; // role.Description; + this.serverRoles.Add(role.Name, new ServerRoleInfo(roleDescription, isRoleMember)); + } + } + catch (Exception ex) + { + System.Diagnostics.Trace.TraceError(ex.Message); + + // swallow the exception - this method gets called before the dialog is fully created, so there + // is no way to display the error message + } + } + + } + + /// + /// Adapter between the CreateLogin dbCommanderPages and the + /// + internal class LoginPrototype + { + private SqlCollationSensitiveStringComparer comparer = null; + + /// + /// string of asterisks to display in lieu of the actual password + /// + public static string fakePassword = "***************"; + + /// + /// Private class encapsulating the data that is changed by the UI. + /// + /// + /// Isolating this data allows for an easy implementation of Reset() and + /// simplifies difference detection when committing changes to the server. + /// + private class LoginPrototypeData : ICloneable + { + #region data members + private string loginName = string.Empty; + private SqlServer.Management.Smo.LoginType loginType = SqlServer.Management.Smo.LoginType.WindowsUser; + + // General data + private string defaultDatabase = "master"; + private string defaultLanguage = String.Empty; + private ServerRoles serverRoles = null; + private HybridDictionary databaseRolesCollection = null; + + // Windows Authentication data + private bool windowsGrantAccess = true; + + // SQL Authentication data + private string sqlPassword = string.Empty; + private string sqlPasswordConfirm = string.Empty; + private string oldPassword = string.Empty; + private bool showOldPassword = false; + + // yukon only + private bool mustChange = true; + private bool isDisabled = false; + private bool isLockedOut = false; + private bool enforcePolicy = true; + private bool enforceExpiration = true; + + // Certificate and Asymmetric Key based + private string certificateName = String.Empty; + private string asymmetricKeyName = String.Empty; + + private bool initialized = false; + private Login login = null; + private Microsoft.SqlServer.Management.Smo.Server server; + private static string defaultLanguageDisplay; + private bool windowsAuthSupported = true; + + private StringCollection credentials = null; + #endregion + + #region Properties + + // General properties + + public SqlServer.Management.Smo.LoginType LoginType + { + get + { + if (!this.initialized) + { + LoadData(); + } + + return this.loginType; + } + + set + { + this.loginType = value; + } + } + + public string LoginName + { + get + { + if (!this.initialized) + { + LoadData(); + } + + return this.loginName; + } + + set + { + System.Diagnostics.Debug.Assert(this.initialized, "unexpected property set before initialization"); + this.loginName = value; + } + } + + public string DefaultDatabase + { + get + { + if (!this.initialized) + { + LoadData(); + } + + return this.defaultDatabase; + } + + set + { + System.Diagnostics.Debug.Assert(this.initialized, "unexpected property set before initialization"); + this.defaultDatabase = value; + } + } + + public string DefaultLanguage + { + get + { + if (!this.initialized) + { + LoadData(); + } + + return this.defaultLanguage; + } + + set + { + System.Diagnostics.Debug.Assert(this.initialized, "unexpected property set before initialization"); + this.defaultLanguage = value; + } + } + + public ServerRoles ServerRoles + { + get + { + if (!this.initialized) + { + LoadData(); + } + + return this.serverRoles; + } + } + + public HybridDictionary DatabaseRolesCollection + { + get + { + if (!this.initialized) + { + LoadData(); + } + + return this.databaseRolesCollection; + } + } + + public bool Exists + { + get + { + return(this.login != null); + } + } + + public Microsoft.SqlServer.Management.Smo.Server Server + { + get + { + return this.server; + } + } + + public Login Login + { + get + { + return this.login; + } + } + + public bool WindowsAuthSupported + { + get + { + if (this.server.DatabaseEngineEdition == DatabaseEngineEdition.SqlManagedInstance) + { + this.windowsAuthSupported = false; + } + + return this.windowsAuthSupported; + } + } + + public static string DefaultLanguageDisplay + { + get + { + return defaultLanguageDisplay; + } + } + + // Windows Authentication properties + + public bool WindowsGrantAccess + { + get + { + if (!this.initialized) + { + LoadData(); + } + + return this.windowsGrantAccess; + } + + set + { + System.Diagnostics.Debug.Assert(this.initialized, "unexpected property set before initialization"); + this.windowsGrantAccess = value; + } + } + + // SQL Authentication properties + + public string SqlPassword + { + get + { + if (!this.initialized) + { + LoadData(); + } + + return this.sqlPassword; + } + + set + { + System.Diagnostics.Debug.Assert(this.initialized, "unexpected property set before initialization"); + this.sqlPassword = value; + } + } + + public string SqlPasswordConfirm + { + get + { + if (!this.initialized) + { + LoadData(); + } + + return this.sqlPasswordConfirm; + } + + set + { + System.Diagnostics.Debug.Assert(this.initialized, "unexpected property set before initialization"); + this.sqlPasswordConfirm = value; + } + } + + public string OldPassword + { + get + { + if (!this.initialized) + { + LoadData(); + } + + return this.oldPassword; + } + + set + { + System.Diagnostics.Debug.Assert(this.initialized, "unexpected property set before initialization"); + this.oldPassword = value; + } + } + + public bool ShowOldPassword + { + get + { + if (!this.initialized) + { + LoadData(); + } + return this.showOldPassword; + } + set + { + System.Diagnostics.Debug.Assert(this.initialized, "unexpected property set before initialzation"); + this.showOldPassword = value; + } + } + + + public bool MustChange + { + get + { + if (!this.initialized) + { + LoadData(); + } + + return this.mustChange; + } + set + { + System.Diagnostics.Debug.Assert(this.initialized, "unexpected property set before initialization"); + System.Diagnostics.Debug.Assert(Server.Information.Version.Major>=9); + this.mustChange = value; + } + } + + public bool IsDisabled + { + get + { + if (!this.initialized) + { + LoadData(); + } + + return this.isDisabled; + } + set + { + System.Diagnostics.Debug.Assert(this.initialized, "unexpected property set before initialization"); + System.Diagnostics.Debug.Assert(Server.Information.Version.Major>=9); + this.isDisabled = value; + } + } + + public bool IsLockedOut + { + get + { + if (!this.initialized) + { + LoadData(); + } + + return this.isLockedOut; + } + + set + { + System.Diagnostics.Debug.Assert(this.initialized, "unexpected property set before initialization"); + System.Diagnostics.Debug.Assert(Server.Information.Version.Major >= 9); + this.isLockedOut = value; + } + } + + public bool EnforcePolicy + { + get + { + if (!this.initialized) + { + LoadData(); + } + + return this.enforcePolicy; + } + set + { + System.Diagnostics.Debug.Assert(this.initialized, "unexpected property set before initialization"); + System.Diagnostics.Debug.Assert(Server.Information.Version.Major>=9); + this.enforcePolicy = value; + } + } + + public bool EnforceExpiration + { + get + { + if (!this.initialized) + { + LoadData(); + } + + return this.enforceExpiration; + } + set + { + System.Diagnostics.Debug.Assert(this.initialized, "unexpected property set before initialization"); + System.Diagnostics.Debug.Assert(Server.Information.Version.Major>=9); + this.enforceExpiration = value; + } + } + + // Certificate and Asymmtric Key properties + public string CertificateName + { + get + { + return this.certificateName; + } + set + { + this.certificateName = value; + } + } + + public string AsymmetricKeyName + { + get + { + return this.asymmetricKeyName; + } + set + { + this.asymmetricKeyName = value; + } + } + + public StringCollection Credentials + { + get + { + if (!this.initialized) + { + LoadData(); + } + return this.credentials; + } + set + { + if (Server.Information.Version.Major < 10) + System.Diagnostics.Debug.Assert(value.Count <= 1, "Max one credential can be mapped to a login in server < Katmai"); + this.credentials.Clear(); + foreach (string str in value) + { + this.credentials.Add(str); + } + } + } + + #endregion + + static LoginPrototypeData() + { + // ResourceManager resourceManager = new ResourceManager( + // "Microsoft.SqlServer.Management.SqlManagerUI.CreateLoginStrings", + // typeof(LoginPrototype).Assembly); + + // defaultLanguageDisplay = resourceManager.GetString("prototype.defaultLanguage"); + } + + /// + /// private default contructor - used by Clone() + /// + private LoginPrototypeData() + { + } + + /// + /// constructor + /// + /// The server on which we are creating a new login + public LoginPrototypeData(Microsoft.SqlServer.Management.Smo.Server server) + { + this.server = server; + if (server.HostPlatform != HostPlatformNames.Windows) + { + LoginType = SqlServer.Management.Smo.LoginType.SqlLogin; + } + } + + /// + /// constructor + /// + /// The server on which we are modifying a login + /// The login we are modifying + public LoginPrototypeData(Microsoft.SqlServer.Management.Smo.Server server, Login login) + { + this.server = server; + this.login = login; + } + + /// + /// Create a clone of this LoginPrototypeData object + /// + /// The clone LoginPrototypeData object + public object Clone() + { + LoginPrototypeData result = new LoginPrototypeData(); + + result.loginName = this.loginName; + result.loginType = this.loginType; + + result.defaultDatabase = this.defaultDatabase; + result.defaultLanguage = this.defaultLanguage; + result.serverRoles = (this.serverRoles != null) ? (ServerRoles) this.serverRoles.Clone() : null; + + if (this.credentials != null) + { + result.credentials = new StringCollection(); + foreach (string credential in this.credentials) + { + result.credentials.Add(credential); + } + } + + if (this.databaseRolesCollection != null) + { + result.databaseRolesCollection = new HybridDictionary(); + + foreach (string databaseName in this.databaseRolesCollection.Keys) + { + DatabaseRoles roles = (DatabaseRoles) this.databaseRolesCollection[databaseName]; + + result.databaseRolesCollection[databaseName] = roles.Clone(); + } + } + else + { + result.databaseRolesCollection = null; + } + + result.windowsGrantAccess = this.windowsGrantAccess; + + result.sqlPassword = this.sqlPassword; + result.sqlPasswordConfirm = this.sqlPasswordConfirm; + result.oldPassword = this.oldPassword; + result.showOldPassword = this.showOldPassword; + + result.mustChange = this.mustChange; + result.isDisabled = this.isDisabled; + result.enforcePolicy = this.enforcePolicy; + result.enforceExpiration = this.enforceExpiration; + + result.certificateName = this.certificateName; + result.asymmetricKeyName = this.asymmetricKeyName; + + result.initialized = this.initialized; + result.server = this.server; + result.login = this.login; + + return result; + } + + + private void LoadData() + { + this.initialized = true; + + if (this.Exists) + { + LoadExisting(); + } + else + { + LoadNew(); + } + } + + private void LoadExisting() + { + System.Diagnostics.Debug.Assert(server != null, "server is null"); + System.Diagnostics.Debug.Assert(login != null, "login is null"); + + this.loginName = login.Name; + this.loginType = login.LoginType; + + bool useWindowsAuthentication = + (login.LoginType == SqlServer.Management.Smo.LoginType.WindowsUser) || + (login.LoginType == SqlServer.Management.Smo.LoginType.WindowsGroup); + + bool useSqlAuthentication = (login.LoginType == SqlServer.Management.Smo.LoginType.SqlLogin); + + this.windowsGrantAccess = !login.DenyWindowsLogin; + + this.sqlPassword = useSqlAuthentication ? LoginPrototype.fakePassword : string.Empty; + this.sqlPasswordConfirm = useSqlAuthentication ? LoginPrototype.fakePassword : string.Empty; + + this.defaultDatabase = login.DefaultDatabase; + this.credentials = new StringCollection(); + + if ((login.Language != null) && (login.Language.Length != 0)) + { + this.defaultLanguage = login.Language; + } + else + { + this.defaultLanguage = LoginPrototypeData.DefaultLanguageDisplay; + } + + bool isYukon = (9 <= server.Information.Version.Major); + + if (isYukon) + { + if (login.LoginType == SqlServer.Management.Smo.LoginType.SqlLogin) + { + // these properties make sense only for Yukon+ with SQL Authentication + this.mustChange = login.MustChangePassword; + this.enforcePolicy = login.PasswordPolicyEnforced; + this.enforceExpiration = login.PasswordExpirationEnabled; + this.isLockedOut = login.IsLocked; + } + else + { + this.mustChange = false; + this.enforcePolicy = false; + this.enforceExpiration = false; + this.isDisabled = false; + this.isLockedOut = false; + } + + this.isDisabled = login.IsDisabled; + + if (login.LoginType == SqlServer.Management.Smo.LoginType.Certificate) + { + this.certificateName = login.Certificate; + } + else if (login.LoginType == SqlServer.Management.Smo.LoginType.AsymmetricKey) + { + this.asymmetricKeyName = login.AsymmetricKey; + } + } + + this.serverRoles = new ServerRoles(server, login.Name); + this.databaseRolesCollection = new HybridDictionary(); + if (server.Information.Version.Major == 9 && !string.IsNullOrEmpty(login.Credential)) + { + this.credentials.Add(login.Credential); + } + else if (server.Information.Version.Major >= 10) + { + this.credentials.Clear(); + foreach (string str in login.EnumCredentials()) + { + this.credentials.Add(str); + } + } + } + + private void LoadNew() + { + if (!WindowsAuthSupported) + { + this.loginType = SqlServer.Management.Smo.LoginType.SqlLogin; + } + + this.defaultLanguage = LoginPrototypeData.DefaultLanguageDisplay; + + this.serverRoles = new ServerRoles(server); + this.databaseRolesCollection = new HybridDictionary(); + this.credentials = new StringCollection(); + } + + } + + private bool exists; + private string machineName; + private LoginPrototypeData currentState; + private LoginPrototypeData originalState; + + private bool mapToCredential; + public bool MapToCredential + { + set + { + this.mapToCredential = value; + } + } + + /// + /// Whether the login already exists on the server + /// + public bool Exists + { + get + { + return this.exists; + } + } + + /// + /// Whether to use NT Authentication. (e.g. true == NT Authentication, false == SQL Authentication) + /// + public SqlServer.Management.Smo.LoginType LoginType + { + get + { + return this.currentState.LoginType; + } + + set + { + System.Diagnostics.Debug.Assert(!this.Exists, "we shouldn't be changing the login type for existing logins"); + this.currentState.LoginType = value; + } + } + + /// + /// The Windows account name for the login (e.g. MYDOMAIN\mysuser) + /// + public string LoginName + { + get + { + if (!this.Exists && this.UseWindowsAuthentication && this.currentState.LoginName.Length != 0) + { + return CUtils.CanonicalizeWindowsLoginName(this.currentState.LoginName); + } + + return this.currentState.LoginName; + } + + set + { + System.Diagnostics.Debug.Assert(!this.Exists, "shouldn't be renaming existing logins"); + + this.UpdateDefaultSchemaAndUserNames(value); + this.currentState.LoginName = value; + } + } + + /// + /// If the login is a windows login, returns the windows login name with domain name capitalized; + /// otherwise returns the login name as-is + /// + public string CanonicalizedLoginName + { + get + { + string result = String.Empty; + + if (this.LoginName.Length != 0) + { + result = this.UseWindowsAuthentication ? + CUtils.CanonicalizeWindowsLoginName(this.LoginName) : + this.LoginName; + } + + return result; + } + } + + public bool WindowsAuthSupported + { + get + { + return this.currentState.WindowsAuthSupported; + } + } + + /// + /// Whether the Windows account is granted server access (e.g. true == grant access, false == deny access) + /// + public bool WindowsGrantAccess + { + get + { + return this.currentState.WindowsGrantAccess; + } + + set + { + this.currentState.WindowsGrantAccess = value; + } + } + + /// + /// The password associated with the SQL login + /// + public string SqlPassword + { + get + { + return this.currentState.SqlPassword.ToString(); + } + + set + { + this.currentState.SqlPassword = value; + } + } + + /// + /// The password confirmation, which should be the same as the password + /// + public string SqlPasswordConfirm + { + get + { + return this.currentState.SqlPasswordConfirm.ToString(); + } + + set + { + this.currentState.SqlPasswordConfirm = value; + } + } + + /// + /// The login's current password + /// + public string OldPassword + { + get + { + return this.currentState.OldPassword.ToString(); + } + + set + { + this.currentState.OldPassword = value; + } + } + + /// + /// If the old password is to be considered or not + /// + public bool ShowOldPassword + { + get + { + return this.currentState.ShowOldPassword; + } + set + { + this.currentState.ShowOldPassword = value; + } + } + + + /// + /// The default database for the login + /// + public string DefaultDatabase + { + get + { + return this.currentState.DefaultDatabase; + } + + set + { + this.currentState.DefaultDatabase = value; + } + } + + /// + /// The default language for the login + /// + public string DefaultLanguage + { + get + { + return this.currentState.DefaultLanguage; + } + + set + { + this.currentState.DefaultLanguage = value; + } + } + + /// + /// Yukon+ only + /// + /// Password must change at next login + /// + public bool MustChange + { + get + { + return this.currentState.MustChange; + } + set + { + this.currentState.MustChange = value; + } + } + + /// + /// Yukon+ only + /// + /// Is login disabled? + /// + public bool IsDisabled + { + get + { + return this.currentState.IsDisabled; + } + set + { + this.currentState.IsDisabled = value; + } + } + + /// + /// Yukon+ only + /// + /// Is login locked out? + /// + public bool IsLockedOut + { + get + { + return this.currentState.IsLockedOut; + } + set + { + this.currentState.IsLockedOut = value; + } + } + + /// + /// Yukon+ only + /// + /// enforce system policy on sql login's password + /// + public bool EnforcePolicy + { + get + { + return this.currentState.EnforcePolicy; + } + set + { + this.currentState.EnforcePolicy = value; + } + } + + /// + /// Yukon+ only + /// + /// expiration of sql login password + /// + public bool EnforceExpiration + { + get + { + return this.currentState.EnforceExpiration; + } + set + { + this.currentState.EnforceExpiration = value; + } + } + + /// + /// If this is a certificate based login, returns the name of the certificate; + /// otherwise, returns an empty string + /// + public string CertificateName + { + get + { + return this.currentState.CertificateName; + } + set + { + this.currentState.CertificateName = value; + } + } + + /// + /// If this is an asymmetric key based login, returns the name of the key; + /// otherwise, returns an empty string + /// + public string AsymmetricKeyName + { + get + { + return this.currentState.AsymmetricKeyName; + } + set + { + this.currentState.AsymmetricKeyName = value; + } + } + + /// + /// The server roles collection for the login + /// + public ServerRoles ServerRoles + { + get + { + return this.currentState.ServerRoles; + } + } + + public StringCollection Credentials + { + get + { + return this.currentState.Credentials; + } + set + { + this.currentState.Credentials.Clear(); + foreach (string str in value) + { + this.currentState.Credentials.Add(str); + } + } + } + + /// + /// Get the database roles collection for the user in a particular database + /// + /// The name of the database + /// The database roles collection + public DatabaseRoles GetDatabaseRoles(string databaseName) + { + DatabaseRoles result = null; + + if (!this.currentState.DatabaseRolesCollection.Contains(databaseName)) + { + if (this.Exists) + { + result = new DatabaseRoles(this.currentState.Server, databaseName, this.LoginName); + } + else + { + result = new DatabaseRoles(this.currentState.Server, databaseName); + } + this.currentState.DatabaseRolesCollection.Add(databaseName, result); + this.originalState.DatabaseRolesCollection.Add(databaseName, result.Clone()); + } + else + { + result = (DatabaseRoles) this.currentState.DatabaseRolesCollection[databaseName]; + } + + return result; + } + + /// + /// Get the names of the schemas in the indicated database in alphabetical order + /// + /// The name of the database + /// The names of the database's schemas + public StringCollection GetDatabaseSchemaNames(string databaseName) + { + return this.GetDatabaseRoles(databaseName).SchemaNames; + } + + + /// + /// A list of names of the databases on the server + /// + public string[] DatabaseNames + { + get + { + Request request = new Request(); + + request.Urn = "Server/Database"; + request.Fields = new string[1] {"Name"}; + request.OrderByList = new OrderBy[1] { new OrderBy("Name", OrderBy.Direction.Asc)}; + + DataTable databases = new Enumerator().Process(this.currentState.Server.ConnectionContext, request); + int databaseCount = databases.Rows.Count; + string[] result = new string[databaseCount]; + + for (int databaseIndex = 0; databaseIndex < databaseCount; ++databaseIndex) + { + result[databaseIndex] = databases.Rows[databaseIndex][0].ToString(); + } + + return result; + } + } + + /// + /// A list of names of the certificates in master database. + /// + public DataTable CertificateNames + { + get + { + Request request = new Request(); + + request.Urn = "Server/Database[@Name='master']/Certificate"; + request.Fields = new string[1] { "Name" }; + request.OrderByList = new OrderBy[1] { new OrderBy("Name", OrderBy.Direction.Asc) }; + + DataTable certificates = new Enumerator().Process(this.currentState.Server.ConnectionContext, request); + + DataView dv = certificates.DefaultView; + dv.RowFilter = "Name NOT LIKE '##MS%'"; + + return dv.ToTable(); + } + } + + /// + /// A list of names of the certificates in master database. + /// + public DataTable AsymmetricKeyNames + { + get + { + Request request = new Request(); + + request.Urn = "Server/Database[@Name='master']/AsymmetricKey"; + request.Fields = new string[1] { "Name" }; + request.OrderByList = new OrderBy[1] { new OrderBy("Name", OrderBy.Direction.Asc) }; + + DataTable asymmetricKeys = new Enumerator().Process(this.currentState.Server.ConnectionContext, request); + + return asymmetricKeys; + } + } + + + public Hashtable credProviderMap; + public StringCollection CredentialNames + { + get + { + if (this.currentState.Server.Information.Version.Major < 9) + return null; + + bool isKatmai = (this.currentState.Server.Version.Major >= 10); + Request request = new Request(); + request.Urn = "Server/Credential"; + if (isKatmai) + { + request.Fields = new string[2] { "Name", "ProviderName" }; + } + else + { + request.Fields = new string[1] { "Name" }; + } + DataTable dt = new Enumerator().Process(this.currentState.Server.ConnectionContext, request); + StringCollection result = new StringCollection(); + credProviderMap = new Hashtable(); + foreach (DataRow dr in dt.Rows) + { + result.Add(dr["Name"].ToString()); + if (isKatmai) + { + credProviderMap.Add(dr["Name"].ToString(), dr["ProviderName"].ToString()); + } + else + { + credProviderMap.Add(dr["Name"].ToString(), string.Empty); + } + } + return result; + } + } + + /// + /// constructor + /// + /// The server on which we are creating a login + public LoginPrototype(Microsoft.SqlServer.Management.Smo.Server server) + { + this.exists = false; + this.machineName = server.ConnectionContext.TrueName.ToUpperInvariant(); + this.currentState = new LoginPrototypeData(server); + this.originalState = (LoginPrototypeData) this.currentState.Clone(); + this.comparer = new SqlCollationSensitiveStringComparer(server.Information.Collation); + } + + /// + /// constructor + /// + /// The server on which we are modifying a login + /// The login we are modifying + public LoginPrototype(Microsoft.SqlServer.Management.Smo.Server server, Login login) + { + this.exists = true; + this.machineName = server.ConnectionContext.TrueName.ToUpperInvariant(); + this.currentState = new LoginPrototypeData(server, login); + this.originalState = (LoginPrototypeData) this.currentState.Clone(); + this.comparer = new SqlCollationSensitiveStringComparer(server.Information.Collation); + } + + /// + /// constructor + /// + /// The server on which we are creating a login + public LoginPrototype(Microsoft.SqlServer.Management.Smo.Server server, LoginInfo login) + { + this.exists = false; + this.machineName = server.ConnectionContext.TrueName.ToUpperInvariant(); + this.currentState = new LoginPrototypeData(server); + this.originalState = (LoginPrototypeData) this.currentState.Clone(); + this.comparer = new SqlCollationSensitiveStringComparer(server.Information.Collation); + + this.LoginName = login.LoginName; + this.SqlPassword = login.Password; + this.OldPassword = login.OldPassword; + this.LoginType = SqlServer.Management.Smo.LoginType.SqlLogin; + this.DefaultLanguage = login.DefaultLanguage; + this.DefaultDatabase = login.DefaultDatabase; + } + + /// + /// Reset the prototype state to its initial state + /// + public void Reset(Microsoft.SqlServer.Management.Smo.Server server) + { + if (this.Exists) + { + this.Reload(server); + } + else + { + this.currentState = (LoginPrototypeData) this.originalState.Clone(); + } + } + + public void Reload(Microsoft.SqlServer.Management.Smo.Server server) + { + System.Diagnostics.Debug.Assert(this.Exists, "trying to load the state for a login that doesn't exist"); + + Login login = server.Logins[this.LoginName]; + System.Diagnostics.Debug.Assert(login != null, "login does not exist on the server"); + + this.originalState = new LoginPrototypeData(server, login); + this.currentState = (LoginPrototypeData) this.originalState.Clone(); + + } + + /// + /// Create the login or modify the login's access type, default database, default language, + /// and password + /// map the login to credentials + /// + public void ApplyGeneralChanges(Microsoft.SqlServer.Management.Smo.Server server) + { + bool changesMade = false; + Login login = null; + + // if the login exists, get the login, otherwise make a new login + if (this.Exists) + { + login = server.Logins[this.LoginName]; + if (login == null) + { + throw new ApplicationException("CreateLoginSR.LoginMissing(this.LoginName)"); + } + } + else + { + login = new Login(server, this.CanonicalizedLoginName); + login.LoginType = this.LoginType; + } + + // set whether to grant SQL CONNECT to the login and whether to disable the login + if (this.currentState.WindowsGrantAccess != this.originalState.WindowsGrantAccess) + { + login.DenyWindowsLogin = !this.WindowsGrantAccess; + changesMade = true; + } + + if (9 <= server.Information.Version.Major) + { + if (!this.Exists || (this.currentState.CertificateName != this.originalState.CertificateName)) + { + login.Certificate = this.CertificateName; + changesMade = true; + } + + if (!this.Exists || (this.currentState.AsymmetricKeyName != this.originalState.AsymmetricKeyName)) + { + login.AsymmetricKey = this.AsymmetricKeyName; + changesMade = true; + } + } + + // set the default database and language + if (!this.Exists || (this.currentState.DefaultDatabase != this.originalState.DefaultDatabase)) + { + login.DefaultDatabase = this.DefaultDatabase; + changesMade = true; + } + + if (!this.Exists || (this.currentState.DefaultLanguage != this.originalState.DefaultLanguage)) + { + if (this.DefaultLanguage == LoginPrototypeData.DefaultLanguageDisplay) + { + login.Language = String.Empty; + } + else + { + login.Language = this.DefaultLanguage; + } + + changesMade = true; + } + + // enforcing password policy, enforcing password expiration, and mapping a credential are supported only on Yukon+ + if ((this.LoginType == SqlServer.Management.Smo.LoginType.SqlLogin) && (server.Information.Version.Major >= 9)) + { + if (!this.Exists || (this.currentState.EnforcePolicy != this.originalState.EnforcePolicy)) + { + login.PasswordPolicyEnforced = this.currentState.EnforcePolicy; + changesMade = true; + } + + if (!this.Exists || (this.currentState.EnforceExpiration != this.originalState.EnforceExpiration)) + { + login.PasswordExpirationEnabled = this.currentState.EnforceExpiration; + changesMade = true; + } + } + + if (server.Information.Version.Major == 9) + { + System.Diagnostics.Debug.Assert(this.currentState.Credentials.Count <= 1, "Yukon can have max one credential mapped to a login"); + if (this.currentState.Credentials.Count == 1 //One credential is currently present in the grid + && mapToCredential) //Credential in the grid should be honored. + { + if (!this.originalState.Credentials.Contains(this.currentState.Credentials[0])) //Ensuring that credential is either added or changed. + { + // new credential is added + login.Credential = this.currentState.Credentials[0]; + changesMade = true; + } + } + else if (this.originalState.Credentials.Count == 1 //Originally atleast one credential was present. + && (!mapToCredential //Credential in the grid should be ignored. + || this.currentState.Credentials.Count == 0)) //Grid is empty now. + { + // credential is dropped + login.Credential = string.Empty; + changesMade = true; + } + } + + // create or alter the login + if (this.Exists) + { + if (changesMade) + { + login.Alter(); + } + } + else + { + // if login is a SQL Login and the login is being created, specify the password + if (this.LoginType == SqlServer.Management.Smo.LoginType.SqlLogin) + { + login.Create(this.SqlPassword, this.MustChange ? LoginCreateOptions.MustChange : LoginCreateOptions.None); + } + else + { + login.Create(); + } + } + + // If the login already exists and uses SQL authentication and either + // the password or one of the password options has changed, change + // the password to match. Note that this should be delayed until after + // the "enforce policy" and "enforce expiration" options have been set + // in the call to Login.Alter() above. + if ((this.LoginType == SqlServer.Management.Smo.LoginType.SqlLogin) && this.Exists) + { + if (this.currentState.SqlPassword != this.originalState.SqlPassword) //Password is changed + { + if (server.Information.Version.Major >= 9 + && (this.currentState.IsLockedOut != this.originalState.IsLockedOut + || this.currentState.MustChange != this.originalState.MustChange)) + { + if (this.currentState.IsLockedOut != this.originalState.IsLockedOut //IsLockedOut value has been changed + && !this.currentState.IsLockedOut) //IsLockedOut is false, which means we have to unlock the login now + { + login.ChangePassword(this.SqlPassword, !this.currentState.IsLockedOut, this.MustChange); + } + else //no change in isLockedOut means we don't need to include UNBLOCK Option in the Alter Login query + { + login.ChangePassword(this.SqlPassword, false, this.MustChange); + } + } + else //For server versions < 9, UNLOCK(IsLockedOut) and MUST_CHANGE(MustChange) are not provided + { //For server version >= 9, if we don't change UNLOCK(IsLockedOut) and MUST_CHANGE(MustChange), the T-Sql remains the same as the previous versions + if (!this.currentState.ShowOldPassword) + { + login.ChangePassword(this.SqlPassword); + } + else + { + login.ChangePassword(this.OldPassword, this.SqlPassword); + } + } + } + else //Password should be reset while unlocking + { + if (server.Information.Version.Major >= 9 + && (this.currentState.IsLockedOut != this.originalState.IsLockedOut) + ) + { + throw new ArgumentException("CreateLoginSR.ResetPasswordWhileUnlocking"); + } + } + } + + // enable or disable the login as needed + if (this.currentState.IsDisabled != this.originalState.IsDisabled) + { + if (this.currentState.IsDisabled) + { + login.Disable(); + } + else + { + login.Enable(); + } + } + if (server.Information.Version.Major >= 10) + { + foreach (string dropCredential in this.originalState.Credentials) + { + if (!mapToCredential || !this.currentState.Credentials.Contains(dropCredential)) + { + login.DropCredential(dropCredential); + } + } + foreach (string newCredential in this.currentState.Credentials) + { + if (mapToCredential && !this.originalState.Credentials.Contains(newCredential)) + { + login.AddCredential(newCredential); + } + } + } + } + + /// + /// Set the login's server role membership + /// + public void ApplyServerRoleChanges(Microsoft.SqlServer.Management.Smo.Server server) + { + // get the login + Login login = server.Logins[this.LoginName]; + + // foreach server role + foreach (ServerRole role in server.Roles) + { + // all users are always members of the public role, so skip processing for public + if (0 != String.Compare(role.Name, "public", StringComparison.Ordinal)) + { + bool wasOriginallyARoleMember = this.originalState.ServerRoles.IsMember(role.Name); + bool isCurrentlyARoleMember = this.currentState.ServerRoles.IsMember(role.Name); + + + // if the login is currently a member of the role, but wasn't originally a member, add the login to the role + if (isCurrentlyARoleMember && !wasOriginallyARoleMember) + { + role.AddMember(this.LoginName); + } + // if the login is not currently a member of the role, but originally was a member, remove the login from the role + else if (!isCurrentlyARoleMember && wasOriginallyARoleMember) + { + role.DropMember(this.LoginName); + } + } + } + } + + /// + /// Set the login's database role membership + /// + public void ApplyDatabaseRoleChanges(Microsoft.SqlServer.Management.Smo.Server server) + { + System.Diagnostics.Debug.Assert(server != null, "the server is null"); + + // get the login + Login login = server.Logins[this.LoginName]; + + // foreach database + foreach (Database database in server.Databases) + { + // if anything has been changed, then the currentState will include the database + if (this.currentState.DatabaseRolesCollection.Contains(database.Name)) + { + DatabaseRoles currentDatabaseRoles = (DatabaseRoles) this.currentState.DatabaseRolesCollection[database.Name]; + DatabaseRoles originalDatabaseRoles = (DatabaseRoles) this.originalState.DatabaseRolesCollection[database.Name]; + + // if the login is currently permitted in the database + if (currentDatabaseRoles.PermitDatabaseAccess) + { + // get the existing user for the login in the database + bool userRecreated = false; + string existingUserName = originalDatabaseRoles.UserName; + User user = + ((existingUserName != null) && (existingUserName.Length != 0)) ? + database.Users[existingUserName] : + null; + + // If the user doesn't exist, create the user in the database. + if (user == null) + { + // Note that if the user name already exists and is mapped to + // another login, SMO will emit the appropriate error message. + + user = new User(database, currentDatabaseRoles.UserName); + user.Login = this.LoginName; + user.Create(); + } + // if a user does exist in the database for the login and + // the user name has been changed + else if (user.Name != currentDatabaseRoles.UserName) + { + // rename the user to the new name + user.Rename(currentDatabaseRoles.UserName); + } + + System.Diagnostics.Debug.Assert(user != null, "user has not been created"); + + if (currentDatabaseRoles.DefaultSchema != originalDatabaseRoles.DefaultSchema) + { + user.DefaultSchema = currentDatabaseRoles.DefaultSchema; + user.Alter(); + + // if the schema doesn't exist, create it + if ((user.DefaultSchema != null) && (user.DefaultSchema.Length != 0) && + !database.Schemas.Contains(user.DefaultSchema)) + { + Schema schema = new Schema(database, user.DefaultSchema); + schema.Owner = user.Name; + + schema.Create(); + } + } + + // if any of the roles have been changed + if (currentDatabaseRoles.RoleMembershipChanged || userRecreated || !this.Exists) + { + // commit the role changes to the server + foreach (DatabaseRole role in database.Roles) + { + // all users are always members of the public role, so skip processing for public + if (0 != String.Compare(role.Name, "public", StringComparison.Ordinal)) + { + // If the login is new, it is not a member of any role + // + bool wasOriginallyARoleMember = (this.Exists && originalDatabaseRoles.IsMember(role.Name)); + bool isCurrentlyARoleMember = currentDatabaseRoles.IsMember(role.Name); + bool defaultSchemaChanged = (originalDatabaseRoles.DefaultSchema != currentDatabaseRoles.DefaultSchema); + + + + // if the login is currently in the role, but wasn't originally, add the login to the role + if (isCurrentlyARoleMember && (!wasOriginallyARoleMember || userRecreated)) + { + role.AddMember(currentDatabaseRoles.UserName); + } + // if the login is currently not in the role, but originally was, remove the login from the role + if (!isCurrentlyARoleMember && (wasOriginallyARoleMember && !userRecreated)) + { + role.DropMember(originalDatabaseRoles.UserName); + } + } + } + } + } + // else if the login was originally permitted in the database + else if (originalDatabaseRoles.PermitDatabaseAccess) + { + // remove the associated user + string existingUserName = login.GetDatabaseUser(database.Name); + if ((existingUserName != null) && (existingUserName.Length != 0)) + { + System.Diagnostics.Debug.Assert(database.Users[existingUserName] != null, "user not found in collection"); + database.Users[existingUserName].Drop(); + } + } + } + } + } + + /// + /// Remove the login from all database roles in the named database + /// + /// The name of the database from which we are removing login + internal void DisjoinAllDatabaseRoles(string databaseName) + { + DatabaseRoles roles = this.GetDatabaseRoles(databaseName); + + foreach (string roleName in roles.DatabaseRoleNames) + { + roles.SetMember(roleName, false); + } + } + + /// + /// Update any default schemas or user names in the databaseRolesCollection that match + /// the old login name to match a new login name. + /// + /// The new login name + private void UpdateDefaultSchemaAndUserNames(string newLoginName) + { + foreach (DatabaseRoles databaseRoles in this.currentState.DatabaseRolesCollection.Values) + { + if (databaseRoles.PermitDatabaseAccess) + { + // if the default schema name is the same as the login name, + // then set the default schema name to the new login name + if (this.NameMatchesCurrentLoginName(databaseRoles.DefaultSchema)) + { + databaseRoles.DefaultSchema = newLoginName; + } + + // if the user name is the same as the login name, + // then set the user name to the new login name + if (this.NameMatchesCurrentLoginName(databaseRoles.UserName)) + { + databaseRoles.UserName = newLoginName; + } + + } + } + } + + /// + /// Does the input name match the current login name + /// + /// The name to compare + /// True if they match, otherwise false + private bool NameMatchesCurrentLoginName(string otherName) + { + // if the login is not a windows login, do a collation-sensitive comparison. + // + // if the login is a windows login, do a case-insensitive comparison + // because domain\user is equivalent to the canonicalized form of DOMAIN\user. + + bool matchesSqlLogin = + !this.UseWindowsAuthentication && + (0 == this.comparer.Compare(this.CanonicalizedLoginName, otherName)); + + bool matchesWindowsLogin = + this.UseWindowsAuthentication && + (0 == string.Compare(this.CanonicalizedLoginName, otherName, StringComparison.OrdinalIgnoreCase)); + + return (matchesSqlLogin || matchesWindowsLogin); + } + + /// + /// tells us if user changed the password (password initial state different then current state) + /// this is required by ui since it needs to enable disable some checkboxes based on this info + /// + internal bool PasswordWasChanged + { + get + { + if ( (!this.Exists) || (this.currentState.SqlPassword != this.originalState.SqlPassword)) + { + return true; + } + return false; + } + } + + public bool UseWindowsAuthentication + { + get + { + return ((this.LoginType == SqlServer.Management.Smo.LoginType.WindowsUser) || (this.LoginType == SqlServer.Management.Smo.LoginType.WindowsGroup)); + } + } + + public bool UseAadAuthentication + { + get + { + return ((this.LoginType == SqlServer.Management.Smo.LoginType.ExternalUser) || (this.LoginType == SqlServer.Management.Smo.LoginType.ExternalGroup)); + } + } + } + + /// + /// Case-sensitive, culture-invariant string comparer + /// + internal class CaseSensitiveCultureInvariantComparer : IComparer + { + public CaseSensitiveCultureInvariantComparer() {} + + /// + /// Compare strings a and b + /// + /// First string + /// Second string + /// less than zero if a is less than b, 0 if a and b are equal, and greater than zero if a is greater than b + public int Compare(object a, object b) + { + if ((a == null) || (b == null) || !(a is String) || !(b is String)) + { + throw new ArgumentException(); + } + + string string_a = (string) a; + string string_b = (string) b; + + return String.Compare(string_a, string_b, StringComparison.Ordinal); + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Security/SecurityService.cs b/src/Microsoft.SqlTools.ServiceLayer/Security/SecurityService.cs index e83edc4b..80e4c562 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Security/SecurityService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Security/SecurityService.cs @@ -4,7 +4,15 @@ // using System; +using System.Collections; +using System.Collections.Specialized; +using System.Data; +using System.Security; using System.Threading.Tasks; +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.Dmf; +using Microsoft.SqlServer.Management.Sdk.Sfc; +using Microsoft.SqlServer.Management.Smo; using Microsoft.SqlTools.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Hosting; @@ -79,8 +87,356 @@ namespace Microsoft.SqlTools.ServiceLayer.Security this.ServiceHost.SetRequestHandler(UpdateCredentialRequest.Type, HandleUpdateCredentialRequest, true); this.ServiceHost.SetRequestHandler(DeleteCredentialRequest.Type, HandleDeleteCredentialRequest, true); this.ServiceHost.SetRequestHandler(GetCredentialsRequest.Type, HandleGetCredentialsRequest, true); + + // Login request handlers + this.ServiceHost.SetRequestHandler(CreateLoginRequest.Type, HandleCreateLoginRequest, true); + this.ServiceHost.SetRequestHandler(DeleteLoginRequest.Type, HandleDeleteLoginRequest, true); } + +#region "Login Handlers" + + /// + /// Handle request to create a login + /// + internal async Task HandleCreateLoginRequest(CreateLoginParams parameters, RequestContext requestContext) + { + ConnectionInfo connInfo; + ConnectionServiceInstance.TryFindConnection(parameters.OwnerUri, out connInfo); + // if (connInfo == null) + // { + // // raise an error + // } + + CDataContainer dataContainer = CDataContainer.CreateDataContainer(connInfo, databaseExists: true); + LoginPrototype prototype = new LoginPrototype(dataContainer.Server, parameters.Login); + + if (prototype.LoginType == SqlServer.Management.Smo.LoginType.SqlLogin) + { + // check that there is a password + // this check is made if policy enforcement is off + // with policy turned on we do not display this message, instead we let server + // return the error associated with null password (coming from policy) - see bug 124377 + if (prototype.SqlPassword.Length == 0 && prototype.EnforcePolicy == false) + { + // raise error here + } + + // check that password and confirm password controls' text matches + if (0 != String.Compare(prototype.SqlPassword, prototype.SqlPasswordConfirm, StringComparison.Ordinal)) + { + // raise error here + } + } + + prototype.ApplyGeneralChanges(dataContainer.Server); + + await requestContext.SendResult(new CreateLoginResult() + { + Login = parameters.Login, + Success = true, + ErrorMessage = string.Empty + }); + } + + /// + /// Handle request to delete a credential + /// + internal async Task HandleDeleteLoginRequest(DeleteLoginParams parameters, RequestContext requestContext) + { + ConnectionInfo connInfo; + ConnectionServiceInstance.TryFindConnection(parameters.OwnerUri, out connInfo); + // if (connInfo == null) + // { + // // raise an error + // } + + CDataContainer dataContainer = CDataContainer.CreateDataContainer(connInfo, databaseExists: true); + Login login = dataContainer.Server.Logins[parameters.LoginName]; + + dataContainer.SqlDialogSubject = login; + DoDropObject(dataContainer); + + await requestContext.SendResult(new ResultStatus() + { + Success = true, + ErrorMessage = string.Empty + }); + } + +#endregion + +#region "User Handlers" + + private UserPrototypeNew InitUserNew(CDataContainer dataContainer) + { + // this.DataContainer = context; + // this.parentDbUrn = new Urn(this.DataContainer.ParentUrn); + // this.objectUrn = new Urn(this.DataContainer.ObjectUrn); + ExhaustiveUserTypes currentUserType; + UserPrototypeFactory userPrototypeFactory = UserPrototypeFactory.GetInstance(dataContainer); + + if (dataContainer.IsNewObject) + { + if (IsParentDatabaseContained(dataContainer.ParentUrn, dataContainer)) + { + currentUserType = ExhaustiveUserTypes.SqlUserWithPassword; + } + else + { + currentUserType = ExhaustiveUserTypes.LoginMappedUser; + } + } + else + { + currentUserType = this.GetCurrentUserTypeForExistingUser( + dataContainer.Server.GetSmoObject(dataContainer.ObjectUrn) as User); + } + + UserPrototypeNew currentUserPrototype = userPrototypeFactory.GetUserPrototype(currentUserType); + return currentUserPrototype; + } + + private ExhaustiveUserTypes GetCurrentUserTypeForExistingUser(User user) + { + switch (user.UserType) + { + case UserType.SqlUser: + if (user.IsSupportedProperty("AuthenticationType")) + { + if (user.AuthenticationType == AuthenticationType.Windows) + { + return ExhaustiveUserTypes.WindowsUser; + } + else if (user.AuthenticationType == AuthenticationType.Database) + { + return ExhaustiveUserTypes.SqlUserWithPassword; + } + } + + return ExhaustiveUserTypes.LoginMappedUser; + + case UserType.NoLogin: + return ExhaustiveUserTypes.SqlUserWithoutLogin; + + case UserType.Certificate: + return ExhaustiveUserTypes.CertificateMappedUser; + + case UserType.AsymmetricKey: + return ExhaustiveUserTypes.AsymmetricKeyMappedUser; + + default: + return ExhaustiveUserTypes.Unknown; + } + } + + private bool IsParentDatabaseContained(Urn parentDbUrn, CDataContainer dataContainer) + { + string parentDbName = parentDbUrn.GetNameForType("Database"); + Database parentDatabase = dataContainer.Server.Databases[parentDbName]; + + if (parentDatabase.IsSupportedProperty("ContainmentType") + && parentDatabase.ContainmentType == ContainmentType.Partial) + { + return true; + } + + return false; + } + + + private void GetUserTypeOptions(CDataContainer dataContainer) + { + if (SqlMgmtUtils.IsSql11OrLater(dataContainer.Server.ServerVersion) + && IsParentDatabaseContained(dataContainer.ParentUrn, dataContainer)) + { + // this.userTypeComboBox.Items.AddRange( + // new string[]{ + // UserSR.SqlUserWithPasswordUserTypeText + // } + //); + } + if (SqlMgmtUtils.IsYukonOrAbove(dataContainer.Server)) + { + // this.userTypeComboBox.Items.AddRange( + // new string[]{ + // UserSR.AsymmetricKeyUserTypeText, + // UserSR.CertificateUserTypeText, + // UserSR.WithoutLoginSqlUserTypeText, + // UserSR.WindowsUserTypeText + // } + // ); + } + // this.userTypeComboBox.Items.AddRange( + // new string[]{ + // UserSR.LoginMappedSqlUserTypeText + // } + // ); + } + + private void GetDefaultLanguageOptions(CDataContainer dataContainer) + { + // this.defaultLanguageComboBox.Items.Clear(); + // this.defaultLanguageComboBox.Items.Add(defaultLanguagePlaceholder); + + // sort the languages alphabetically by alias + SortedList sortedLanguages = new SortedList(Comparer.Default); + + LanguageUtils.SetLanguageDefaultInitFieldsForDefaultLanguages(dataContainer.Server); + foreach (Language language in dataContainer.Server.Languages) + { + LanguageDisplay listValue = new LanguageDisplay(language); + sortedLanguages.Add(language.Alias, listValue); + } + + // add the language display objects to the combo box + foreach (LanguageDisplay languageDisplay in sortedLanguages.Values) + { + //this.defaultLanguageComboBox.Items.Add(languageDisplay); + } + } + + private SecureString GetReadOnlySecureString(string secret) + { + SecureString ss = new SecureString(); + foreach (char c in secret.ToCharArray()) + { + ss.AppendChar(c); + } + ss.MakeReadOnly(); + + return ss; + } + + public void UserMemberships_OnRunNow(object sender, CDataContainer dataContainer) + { + UserPrototypeNew currentPrototype = UserPrototypeFactory.GetInstance(dataContainer).CurrentPrototype; + + //In case the UserGeneral/OwnedSchemas pages are loaded, + //those will takes care of applying membership changes also. + //Hence, we only need to apply changes in this method when those are not loaded. + if (!currentPrototype.IsRoleMembershipChangesApplied) + { + //base.OnRunNow(sender); + + User user = currentPrototype.ApplyChanges(); + + //this.ExecutionMode = ExecutionMode.Success; + dataContainer.ObjectName = currentPrototype.Name; + dataContainer.SqlDialogSubject = user; + } + + //setting back to original after changes are applied + currentPrototype.IsRoleMembershipChangesApplied = false; + } + + /// + /// implementation of OnPanelRunNow + /// + /// + public void UserOwnedSchemas_OnRunNow(object sender, CDataContainer dataContainer) + { + UserPrototypeNew currentPrototype = UserPrototypeFactory.GetInstance(dataContainer).CurrentPrototype; + + //In case the UserGeneral/Membership pages are loaded, + //those will takes care of applying schema ownership changes also. + //Hence, we only need to apply changes in this method when those are not loaded. + if (!currentPrototype.IsSchemaOwnershipChangesApplied) + { + //base.OnRunNow(sender); + + User user = currentPrototype.ApplyChanges(); + + //this.ExecutionMode = ExecutionMode.Success; + dataContainer.ObjectName = currentPrototype.Name; + dataContainer.SqlDialogSubject = user; + } + + //setting back to original after changes are applied + currentPrototype.IsSchemaOwnershipChangesApplied = false; + } + + // how to populate defaults from prototype, will delete once refactored + // private void InitializeValuesInUiControls() + // { + // this.userNameTextBox.Text = this.currentUserPrototype.Name; + + // if(this.currentUserPrototype.UserType == UserType.Certificate) + // { + // this.mappedObjTextbox.Text = this.currentUserPrototype.CertificateName; + // } + // if (this.currentUserPrototype.UserType == UserType.AsymmetricKey) + // { + // this.mappedObjTextbox.Text = this.currentUserPrototype.AsymmetricKeyName; + // } + // IUserPrototypeWithMappedLogin mappedLoginPrototype = this.currentUserPrototype + // as IUserPrototypeWithMappedLogin; + // if (mappedLoginPrototype != null) + // { + // this.mappedObjTextbox.Text = mappedLoginPrototype.LoginName; + // } + + // IUserPrototypeWithDefaultLanguage defaultLanguagePrototype = this.currentUserPrototype + // as IUserPrototypeWithDefaultLanguage; + // if (defaultLanguagePrototype != null + // && defaultLanguagePrototype.IsDefaultLanguageSupported) + // { + // string defaultLanguageAlias = defaultLanguagePrototype.DefaultLanguageAlias; + + // //If engine returns default language as empty or null, that means the default language of + // //database will be used. + // //Default language is not applicable for users inside an uncontained authentication. + // if (string.IsNullOrEmpty(defaultLanguageAlias) + // && (this.DataContainer.Server.GetSmoObject(this.parentDbUrn) as Database).ContainmentType != ContainmentType.None) + // { + // defaultLanguageAlias = this.defaultLanguagePlaceholder; + // } + // this.defaultLanguageComboBox.Text = defaultLanguageAlias; + // } + + // IUserPrototypeWithDefaultSchema defaultSchemaPrototype = this.currentUserPrototype + // as IUserPrototypeWithDefaultSchema; + // if (defaultSchemaPrototype != null + // && defaultSchemaPrototype.IsDefaultSchemaSupported) + // { + // this.defaultSchemaTextBox.Text = defaultSchemaPrototype.DefaultSchema; + // } + + // IUserPrototypeWithPassword userWithPwdPrototype = this.currentUserPrototype + // as IUserPrototypeWithPassword; + // if (userWithPwdPrototype != null + // && !this.DataContainer.IsNewObject) + // { + // this.passwordTextBox.Text = FAKE_PASSWORD; + // this.confirmPwdTextBox.Text = FAKE_PASSWORD; + // } + // } + // private void UpdateUiControlsOnLoad() + // { + // if (!this.DataContainer.IsNewObject) + // { + // this.userNameTextBox.ReadOnly = true; //Rename is not allowed from the dialog. + // this.userSearchButton.Enabled = false; + // this.mappedObjTextbox.ReadOnly = true; //Changing mapped login, certificate and asymmetric key is not allowed + // this.mappedObjSearchButton.Enabled = false; + // //from SMO also. + // this.userTypeComboBox.Enabled = false; + // this.oldPasswordTextBox.ReadOnly = true; + // } + // else + // { + // //Old password is only useful for changing the password. + // this.specifyOldPwdCheckBox.Enabled = false; + // this.oldPasswordLabel.Enabled = false; + // this.oldPasswordTextBox.Enabled = false; + // } + // } + + +#endregion + +#region "Credential Handlers" + /// /// Handle request to create a credential /// @@ -183,6 +539,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Security } } +#endregion + #region "Helpers" internal Task> ConfigureCredential( @@ -214,6 +572,376 @@ namespace Microsoft.SqlTools.ServiceLayer.Security }); } + /// + /// this is the main method that is called by DropAllObjects for every object + /// in the grid + /// + /// + private void DoDropObject(CDataContainer dataContainer) + { + var executionMode = dataContainer.Server.ConnectionContext.SqlExecutionModes; + var subjectExecutionMode = executionMode; + + //For Azure the ExecutionManager is different depending on which ExecutionManager + //used - one at the Server level and one at the Database level. So to ensure we + //don't use the wrong execution mode we need to set the mode for both (for on-prem + //this will essentially be a no-op) + SqlSmoObject sqlDialogSubject = null; + try + { + sqlDialogSubject = dataContainer.SqlDialogSubject; + } + catch (System.Exception) + { + //We may not have a valid dialog subject here (such as if the object hasn't been created yet) + //so in that case we'll just ignore it as that's a normal scenario. + } + if (sqlDialogSubject != null) + { + subjectExecutionMode = + sqlDialogSubject.ExecutionManager.ConnectionContext.SqlExecutionModes; + } + + Urn objUrn = sqlDialogSubject.Urn; + System.Diagnostics.Debug.Assert(objUrn != null); + + SfcObjectQuery objectQuery = new SfcObjectQuery(dataContainer.Server); + + IDroppable droppableObj = null; + string[] fields = null; + + foreach( object obj in objectQuery.ExecuteIterator( new SfcQueryExpression( objUrn.ToString() ), fields, null ) ) + { + System.Diagnostics.Debug.Assert(droppableObj == null, "there is only one object"); + droppableObj = obj as IDroppable; + } + + // For Azure databases, the SfcObjectQuery executions above may have overwritten our desired execution mode, so restore it + dataContainer.Server.ConnectionContext.SqlExecutionModes = executionMode; + if (sqlDialogSubject != null) + { + sqlDialogSubject.ExecutionManager.ConnectionContext.SqlExecutionModes = subjectExecutionMode; + } + + if (droppableObj == null) + { + string objectName = objUrn.GetAttribute("Name"); + objectName ??= string.Empty; + throw new Microsoft.SqlServer.Management.Smo.MissingObjectException("DropObjectsSR.ObjectDoesNotExist(objUrn.Type, objectName)"); + } + + //special case database drop - see if we need to delete backup and restore history + SpecialPreDropActionsForObject(dataContainer, droppableObj, + deleteBackupRestoreOrDisableAuditSpecOrDisableAudit: false, + dropOpenConnections: false); + + droppableObj.Drop(); + + //special case Resource Governor reconfigure - for pool, external pool, group Drop(), we need to issue + SpecialPostDropActionsForObject(dataContainer, droppableObj); + + } + + private void SpecialPreDropActionsForObject(CDataContainer dataContainer, IDroppable droppableObj, + bool deleteBackupRestoreOrDisableAuditSpecOrDisableAudit, bool dropOpenConnections) + { + Database db = droppableObj as Database; + + if (deleteBackupRestoreOrDisableAuditSpecOrDisableAudit) + { + if (db != null) + { + dataContainer.Server.DeleteBackupHistory(db.Name); + } + else + { + // else droppable object should be a server or database audit specification + ServerAuditSpecification sas = droppableObj as ServerAuditSpecification; + if (sas != null) + { + sas.Disable(); + } + else + { + DatabaseAuditSpecification das = droppableObj as DatabaseAuditSpecification; + if (das != null) + { + das.Disable(); + } + else + { + Audit aud = droppableObj as Audit; + if (aud != null) + { + aud.Disable(); + } + } + } + } + } + + // special case database drop - drop existing connections to the database other than this one + if (dropOpenConnections) + { + if (db.ActiveConnections > 0) + { + // force the database to be single user + db.DatabaseOptions.UserAccess = DatabaseUserAccess.Single; + db.Alter(TerminationClause.RollbackTransactionsImmediately); + } + } + } + + private void SpecialPostDropActionsForObject(CDataContainer dataContainer, IDroppable droppableObj) + { + if (droppableObj is Policy) + { + Policy policyToDrop = (Policy)droppableObj; + if (!string.IsNullOrEmpty(policyToDrop.ObjectSet)) + { + ObjectSet objectSet = policyToDrop.Parent.ObjectSets[policyToDrop.ObjectSet]; + objectSet.Drop(); + } + } + + ResourcePool rp = droppableObj as ResourcePool; + ExternalResourcePool erp = droppableObj as ExternalResourcePool; + WorkloadGroup wg = droppableObj as WorkloadGroup; + + if (null != rp || null != erp || null != wg) + { + // Alter() Resource Governor to reconfigure + dataContainer.Server.ResourceGovernor.Alter(); + } + } + #endregion // "Helpers" + +// some potentially useful code for working with server & db roles to be refactored later +#region "Roles" + private class SchemaOwnership + { + public bool initiallyOwned; + public bool currentlyOwned; + + public SchemaOwnership(bool initiallyOwned) + { + this.initiallyOwned = initiallyOwned; + this.currentlyOwned = initiallyOwned; + } + } + + private class RoleMembership + { + public bool initiallyAMember; + public bool currentlyAMember; + + public RoleMembership(bool initiallyAMember) + { + this.initiallyAMember = initiallyAMember; + this.currentlyAMember = initiallyAMember; + } + + public RoleMembership(bool initiallyAMember, bool currentlyAMember) + { + this.initiallyAMember = initiallyAMember; + this.currentlyAMember = currentlyAMember; + } + } + + private void DbRole_LoadMembership(string databaseName, string dbroleName, ServerConnection serverConnection) + { + var roleMembers = new HybridDictionary(); + bool isPropertiesMode = false; + if (isPropertiesMode) + { + Enumerator enumerator = new Enumerator(); + Urn urn = String.Format(System.Globalization.CultureInfo.InvariantCulture, + "Server/Database[@Name='{0}']/Role[@Name='{1}']/Member", + Urn.EscapeString(databaseName), + Urn.EscapeString(dbroleName)); + string[] fields = new string[] { "Name" }; + OrderBy[] orderBy = new OrderBy[] { new OrderBy("Name", OrderBy.Direction.Asc)}; + Request request = new Request(urn, fields, orderBy); + DataTable dt = enumerator.Process(serverConnection, request); + + foreach (DataRow dr in dt.Rows) + { + string memberName = dr["Name"].ToString(); + roleMembers[memberName] = new RoleMembership(true); + } + } + } + + /// + /// sends to server user changes related to membership + /// + private void DbRole_SendToServerMembershipChanges(Database db, DatabaseRole dbrole) + { + var roleMembers = new HybridDictionary(); + IDictionaryEnumerator enumerator = roleMembers.GetEnumerator(); + enumerator.Reset(); + + while (enumerator.MoveNext()) + { + DictionaryEntry entry = enumerator.Entry; + string memberName = entry.Key.ToString(); + RoleMembership membership = (RoleMembership) entry.Value; + + if (!membership.initiallyAMember && membership.currentlyAMember) + { + dbrole.AddMember(memberName); + } + else if (membership.initiallyAMember && !membership.currentlyAMember) + { + dbrole.DropMember(memberName); + } + } + } + + private void InitProp(ServerConnection serverConnection, string serverName, string databaseName, + string dbroleName, string dbroleUrn, bool isPropertiesMode) + { + System.Diagnostics.Debug.Assert(serverName!=null); + System.Diagnostics.Debug.Assert((databaseName!=null) && (databaseName.Trim().Length!=0)); + + // LoadSchemas(); + // LoadMembership(); + + if (isPropertiesMode == true) + { + // initialize from enumerator in properties mode + System.Diagnostics.Debug.Assert(dbroleName!=null); + System.Diagnostics.Debug.Assert(dbroleName.Trim().Length !=0); + System.Diagnostics.Debug.Assert(dbroleUrn!=null); + System.Diagnostics.Debug.Assert(dbroleUrn.Trim().Length != 0); + + Enumerator en = new Enumerator(); + Request req = new Request(); + req.Fields = new String [] { "Owner" }; + + if ((dbroleUrn!=null) && (dbroleUrn.Trim().Length != 0)) + { + req.Urn = dbroleUrn; + } + else + { + req.Urn = "Server/Database[@Name='" + Urn.EscapeString(databaseName) + "']/Role[@Name='" + Urn.EscapeString(dbroleName) + "]"; + } + + DataTable dt = en.Process(serverConnection, req); + System.Diagnostics.Debug.Assert(dt!=null); + System.Diagnostics.Debug.Assert(dt.Rows.Count==1); + + if (dt.Rows.Count==0) + { + throw new Exception("DatabaseRoleSR.ErrorDbRoleNotFound"); + } + + // DataRow dr = dt.Rows[0]; + // this.initialOwner = Convert.ToString(dr[DatabaseRoleGeneral.ownerField],System.Globalization.CultureInfo.InvariantCulture); + // this.textBoxOwner.Text = this.initialOwner; + } + } + + private void DbRole_SendDataToServer(CDataContainer dataContainer, string databaseName, + string dbroleName, string ownerName, string initialOwner, string roleName, bool isPropertiesMode) + { + System.Diagnostics.Debug.Assert(databaseName != null && databaseName.Trim().Length != 0, "database name is empty"); + System.Diagnostics.Debug.Assert(dataContainer.Server != null, "server is null"); + + Database database = dataContainer.Server.Databases[databaseName]; + System.Diagnostics.Debug.Assert(database!= null, "database is null"); + + DatabaseRole role = null; + + if (isPropertiesMode == true) // in properties mode -> alter role + { + System.Diagnostics.Debug.Assert(dbroleName != null && dbroleName.Trim().Length != 0, "role name is empty"); + + role = database.Roles[dbroleName]; + System.Diagnostics.Debug.Assert(role != null, "role is null"); + + if (0 != String.Compare(ownerName, initialOwner, StringComparison.Ordinal)) + { + role.Owner = ownerName; + role.Alter(); + } + } + else // not in properties mode -> create role + { + role = new DatabaseRole(database, roleName); + if (ownerName.Length != 0) + { + role.Owner = ownerName; + } + + role.Create(); + } + + // SendToServerSchemaOwnershipChanges(database, role); + // SendToServerMembershipChanges(database, role); + } + + private void DbRole_LoadSchemas(string databaseName, string dbroleName, ServerConnection serverConnection) + { + bool isPropertiesMode = false; + HybridDictionary schemaOwnership = null; + schemaOwnership = new HybridDictionary(); + + Enumerator en = new Enumerator(); + Request req = new Request(); + req.Fields = new String [] { "Name", "Owner" }; + req.Urn = "Server/Database[@Name='" + Urn.EscapeString(databaseName) + "']/Schema"; + + DataTable dt = en.Process(serverConnection, req); + System.Diagnostics.Debug.Assert((dt != null) && (0 < dt.Rows.Count), "enumerator did not return schemas"); + System.Diagnostics.Debug.Assert(!isPropertiesMode || (dbroleName.Length != 0), "role name is not known"); + + foreach (DataRow dr in dt.Rows) + { + string schemaName = Convert.ToString(dr["Name"],System.Globalization.CultureInfo.InvariantCulture); + string schemaOwner = Convert.ToString(dr["Owner"],System.Globalization.CultureInfo.InvariantCulture); + bool roleOwnsSchema = + isPropertiesMode && + (0 == String.Compare(dbroleName, schemaOwner, StringComparison.Ordinal)); + + schemaOwnership[schemaName] = new SchemaOwnership(roleOwnsSchema); + } + } + + /// + /// sends to server changes related to schema ownership + /// + private void DbRole_SendToServerSchemaOwnershipChanges(CDataContainer dataContainer, Database db, DatabaseRole dbrole) + { + HybridDictionary schemaOwnership = null; + if (9 <= dataContainer.Server.Information.Version.Major) + { + IDictionaryEnumerator enumerator = schemaOwnership.GetEnumerator(); + enumerator.Reset(); + while (enumerator.MoveNext()) + { + DictionaryEntry de = enumerator.Entry; + string schemaName = de.Key.ToString(); + SchemaOwnership ownership = (SchemaOwnership)de.Value; + + // If we are creating a new role, then no schema will have been initially owned by this role. + // If we are modifying an existing role, we can only take ownership of roles. (Ownership can't + // be renounced, it can only be positively assigned to a principal.) + if (ownership.currentlyOwned && !ownership.initiallyOwned) + { + Schema schema = db.Schemas[schemaName]; + schema.Owner = dbrole.Name; + schema.Alter(); + } + } + } + } + +#endregion + } + + } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Security/SqlCollationSensitiveStringComparer.cs b/src/Microsoft.SqlTools.ServiceLayer/Security/SqlCollationSensitiveStringComparer.cs new file mode 100644 index 00000000..33f72bbb --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Security/SqlCollationSensitiveStringComparer.cs @@ -0,0 +1,80 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +#region Using directives + +using System.Collections; +using System.Globalization; + +using Microsoft.SqlServer.Management.Smo; + +#endregion + +namespace Microsoft.SqlTools.ServiceLayer.Security +{ + /// + /// String comparer that uses the case sensitivity and other settings + /// from a particular SQL collation + /// +#if DEBUG || EXPOSE_MANAGED_INTERNALS + public +#else + internal +#endif + class SqlCollationSensitiveStringComparer : IComparer + { + private CompareOptions compareOptions; + + /// + /// Constructor + /// + /// The name of the SQL collation, like ALGERIAN_CI_AI + public SqlCollationSensitiveStringComparer(string sqlCollation) + { + if (sqlCollation != null && sqlCollation.Length != 0) + { + this.compareOptions = SqlSupport.GetCompareOptionsFromCollation(sqlCollation); + } + else + { + this.compareOptions = CompareOptions.Ordinal; + } + } + + /// + /// Constructor + /// + /// The CompareOptions for the SQL collation + public SqlCollationSensitiveStringComparer(CompareOptions compareOptions) + { + this.compareOptions = compareOptions; + } + + /// + /// Compare two strings + /// + /// The first string to compare + /// The second string to compare + /// Less than zero if x is less than y, 0 if x equals y, greater than zero if x is greater than y + public int Compare(object x, object y) + { + if (null == x && null == y) + { + return 0; + } + else if (null != x && null == y) + { + return 1; + } + else if (null == x && null != y) + { + return -1; + } + else + { + return CultureInfo.InvariantCulture.CompareInfo.Compare((string) x, (string) y, compareOptions); + } + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Security/UserData.cs b/src/Microsoft.SqlTools.ServiceLayer/Security/UserData.cs new file mode 100644 index 00000000..caca6623 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Security/UserData.cs @@ -0,0 +1,1128 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Security; +using Microsoft.SqlServer.Management.Smo; +using Microsoft.SqlServer.Management.Sdk.Sfc; +using Microsoft.SqlTools.ServiceLayer.Management; + +namespace Microsoft.SqlTools.ServiceLayer.Security +{ + /// + /// Defines the common behavior of all types of database user objects. + /// + internal interface IUserPrototype + { + string Name { get; set; } + UserType UserType { get; set; } + string AsymmetricKeyName { get; set; } + string CertificateName { get; set; } + bool IsSystemObject { get; } + bool Exists { get; } + List SchemaNames { get; } + bool IsSchemaOwner(string schemaName); + void SetSchemaOwner(string schemaName, bool isOwner); + List DatabaseRoleNames { get; } + bool IsRoleMember(string roleName); + void SetRoleMembership(string roleName, bool isMember); + } + + /// + /// Defines the behavior of those database users which have default schema. + /// + internal interface IUserPrototypeWithDefaultSchema + { + bool IsDefaultSchemaSupported { get; } + string DefaultSchema { get; set; } + } + + /// + /// Defines the behavior of those database users which are mapped to a login. + /// + internal interface IUserPrototypeWithMappedLogin + { + string LoginName { get; set; } + } + + /// + /// Defines the behavior of those database users which have password. + /// + internal interface IUserPrototypeWithPassword + { + SecureString Password { set; } + SecureString PasswordConfirm { set; } + SecureString OldPassword { set; } + bool IsOldPasswordRequired { get; set; } + } + + /// + /// Defines the behavior of those database users which have default language. + /// + internal interface IUserPrototypeWithDefaultLanguage + { + bool IsDefaultLanguageSupported { get; } + string DefaultLanguageAlias { get; set; } + } + + /// + /// Object have exhaustive list of data elements which are required for creating + /// any type of database user. + /// + public class UserPrototypeDataNew + { + public string name = string.Empty; + public UserType userType = UserType.SqlUser; + public bool isSystemObject = false; + public Dictionary isSchemaOwned = null; + public Dictionary isMember = null; + + public AuthenticationType authenticationType = AuthenticationType.Instance; + public string mappedLoginName = string.Empty; + public string certificateName = string.Empty; + public string asymmetricKeyName = string.Empty; + public string defaultSchemaName = string.Empty; + public string defaultLanguageAlias = string.Empty; + public SecureString password = new SecureString(); + public SecureString passwordConfirm = new SecureString(); + public SecureString oldPassword = new SecureString(); + public bool isOldPasswordRequired = false; + + /// + /// Used for creating clone of a UserPrototypeData. + /// + private UserPrototypeDataNew() + { + this.isSchemaOwned = new Dictionary(); + this.isMember = new Dictionary(); + } + + public UserPrototypeDataNew(CDataContainer context) + { + this.isSchemaOwned = new Dictionary(); + this.isMember = new Dictionary(); + + if (!context.IsNewObject) + { + this.LoadUserData(context); + } + + this.LoadRoleMembership(context); + + this.LoadSchemaData(context); + } + + public UserPrototypeDataNew Clone() + { + UserPrototypeDataNew result = new UserPrototypeDataNew(); + + result.asymmetricKeyName = this.asymmetricKeyName; + result.authenticationType = this.authenticationType; + result.certificateName = this.certificateName; + result.defaultLanguageAlias = this.defaultLanguageAlias; + result.defaultSchemaName = this.defaultSchemaName; + result.isSystemObject = this.isSystemObject; + result.mappedLoginName = this.mappedLoginName; + result.name = this.name; + result.oldPassword = this.oldPassword; + result.password = this.password; + result.passwordConfirm = this.passwordConfirm; + result.isOldPasswordRequired = this.isOldPasswordRequired; + result.userType = this.userType; + + foreach (string key in this.isMember.Keys) + { + result.isMember[key] = this.isMember[key]; + } + + foreach (string key in this.isSchemaOwned.Keys) + { + result.isSchemaOwned[key] = this.isSchemaOwned[key]; + } + + return result; + } + + public bool HasSameValueAs(UserPrototypeDataNew other) + { + bool result = + (this.asymmetricKeyName == other.asymmetricKeyName) && + (this.authenticationType == other.authenticationType) && + (this.certificateName == other.certificateName) && + (this.defaultLanguageAlias == other.defaultLanguageAlias) && + (this.defaultSchemaName == other.defaultSchemaName) && + (this.isSystemObject == other.isSystemObject) && + (this.mappedLoginName == other.mappedLoginName) && + (this.name == other.name) && + (this.oldPassword == other.oldPassword) && + (this.password == other.password) && + (this.passwordConfirm == other.passwordConfirm) && + (this.isOldPasswordRequired == other.isOldPasswordRequired) && + (this.userType == other.userType); + + result = result && this.isMember.Keys.Count == other.isMember.Keys.Count; + if (result) + { + foreach (string key in this.isMember.Keys) + { + if (this.isMember[key] != other.isMember[key]) + { + result = false; + break; + } + } + } + + result = result && this.isSchemaOwned.Keys.Count == other.isSchemaOwned.Keys.Count; + if (result) + { + foreach (string key in this.isSchemaOwned.Keys) + { + if (this.isSchemaOwned[key] != other.isSchemaOwned[key]) + { + result = false; + break; + } + } + } + + return result; + } + + /// + /// Initializes this object with values from the existing database user. + /// + /// + private void LoadUserData(CDataContainer context) + { + User existingUser = context.Server.GetSmoObject(new Urn(context.ObjectUrn)) as User; + + this.name = existingUser.Name; + this.mappedLoginName = existingUser.Login; + this.isSystemObject = existingUser.IsSystemObject; + + if (SqlMgmtUtils.IsYukonOrAbove(context.Server)) + { + this.asymmetricKeyName = existingUser.AsymmetricKey; + this.certificateName = existingUser.Certificate; + this.defaultSchemaName = existingUser.DefaultSchema; + this.userType = existingUser.UserType; + } + + if (SqlMgmtUtils.IsSql11OrLater(context.Server.ServerVersion)) + { + this.authenticationType = existingUser.AuthenticationType; + this.defaultLanguageAlias = LanguageUtils.GetLanguageAliasFromName(existingUser.Parent.Parent, + existingUser.DefaultLanguage.Name); + } + } + + /// + /// Loads role membership of a database user. + /// + /// + private void LoadRoleMembership(CDataContainer context) + { + Urn objUrn = new Urn(context.ObjectUrn); + Urn databaseUrn = objUrn.Parent; + + Database parentDb = context.Server.GetSmoObject(databaseUrn) as Database; + User existingUser = context.Server.Databases[parentDb.Name].Users[objUrn.GetNameForType("User")]; + + foreach (DatabaseRole dbRole in parentDb.Roles) + { + var comparer = parentDb.GetStringComparer(); + if (comparer.Compare(dbRole.Name, "public") != 0) + { + if (context.IsNewObject) + { + this.isMember[dbRole.Name] = false; + } + else + { + this.isMember[dbRole.Name] = existingUser.IsMember(dbRole.Name); + } + } + } + } + + /// + /// Loads schema ownership related data. + /// + /// + private void LoadSchemaData(CDataContainer context) + { + Urn objUrn = new Urn(context.ObjectUrn); + Urn databaseUrn = objUrn.Parent; + + Database parentDb = context.Server.GetSmoObject(databaseUrn) as Database; + User existingUser = context.Server.Databases[parentDb.Name].Users[objUrn.GetNameForType("User")]; + + if (!SqlMgmtUtils.IsYukonOrAbove(context.Server) + || parentDb.CompatibilityLevel <= CompatibilityLevel.Version80) + { + return; + } + + foreach (Schema sch in parentDb.Schemas) + { + if (context.IsNewObject) + { + this.isSchemaOwned[sch.Name] = false; + } + else + { + var comparer = parentDb.GetStringComparer(); + this.isSchemaOwned[sch.Name] = comparer.Compare(sch.Owner, existingUser.Name) == 0; + } + } + } + } + + /// + /// Prototype object for creating or altering users + /// + internal class UserPrototypeNew : IUserPrototype + { + protected UserPrototypeDataNew originalState = null; + protected UserPrototypeDataNew currentState = null; + + private List schemaNames = null; + private List roleNames = null; + private bool exists = false; + private Database parent = null; + + public bool IsRoleMembershipChangesApplied { get; set; } //default is false + public bool IsSchemaOwnershipChangesApplied { get; set; } //default is false + + #region IUserPrototype Members + + public string Name + { + get + { + return this.currentState.name; + } + set + { + this.currentState.name = value; + } + } + + public string CertificateName + { + get + { + return this.currentState.certificateName; + } + set + { + this.currentState.certificateName = value; + } + } + + public string AsymmetricKeyName + { + get + { + return this.currentState.asymmetricKeyName; + } + set + { + this.currentState.asymmetricKeyName = value; + } + } + + public UserType UserType + { + get + { + return this.currentState.userType; + } + set + { + this.currentState.userType = value; + } + } + + public bool IsSystemObject + { + get + { + return this.currentState.isSystemObject; + } + } + + public bool Exists + { + get + { + return this.exists; + } + } + + public List SchemaNames + { + get + { + return this.schemaNames; + } + } + + public List DatabaseRoleNames + { + get + { + return this.roleNames; + } + } + + public bool IsSchemaOwner(string schemaName) + { + bool isSchemaOwner = false; + this.currentState.isSchemaOwned.TryGetValue(schemaName, out isSchemaOwner); + + return isSchemaOwner; + } + + public void SetSchemaOwner(string schemaName, bool isOwner) + { + this.currentState.isSchemaOwned[schemaName] = isOwner; + } + + public bool IsRoleMember(string roleName) + { + bool isRoleMember = false; + this.currentState.isMember.TryGetValue(roleName, out isRoleMember); + + return isRoleMember; + } + + public void SetRoleMembership(string roleName, bool isMember) + { + this.currentState.isMember[roleName] = isMember; + } + + #endregion + + /// + /// Constructor + /// + /// The context for the dialog + public UserPrototypeNew(CDataContainer context, + UserPrototypeDataNew current, + UserPrototypeDataNew original) + { + this.currentState = current; + this.originalState = original; + + this.exists = !context.IsNewObject; + this.parent = context.Server.GetSmoObject(new Urn(context.ParentUrn)) as Database; + + this.PopulateRoles(); + this.PopulateSchemas(); + } + + private void PopulateRoles() + { + this.roleNames = new List(); + + foreach (DatabaseRole dbRole in this.parent.Roles) + { + var comparer = this.parent.GetStringComparer(); + if (comparer.Compare(dbRole.Name, "public") != 0) + { + this.roleNames.Add(dbRole.Name); + } + } + } + + private void PopulateSchemas() + { + this.schemaNames = new List(); + + if (!SqlMgmtUtils.IsYukonOrAbove(this.parent.Parent) + || this.parent.CompatibilityLevel <= CompatibilityLevel.Version80) + { + return; + } + + foreach (Schema sch in this.parent.Schemas) + { + this.schemaNames.Add(sch.Name); + } + } + + public bool IsYukonOrLater + { + get + { + return SqlMgmtUtils.IsYukonOrAbove(this.parent.Parent); + } + } + + public User ApplyChanges() + { + User user = null; + + user = this.GetUser(); + + if (this.ChangesExist()) + { + this.SaveProperties(user); + this.CreateOrAlterUser(user); + + //Extended Properties page also executes Alter() method on the same user object + //in order to add extended properties. If at that time any property is dirty, + //it will again generate the script corresponding to that. + user.Refresh(); + + this.ApplySchemaOwnershipChanges(user); + this.IsSchemaOwnershipChangesApplied = true; + + this.ApplyRoleMembershipChanges(user); + this.IsRoleMembershipChangesApplied = true; + } + + return user; + } + + protected virtual void CreateOrAlterUser(User user) + { + if (!this.Exists) + { + user.Create(); + } + else + { + user.Alter(); + } + } + + private void ApplySchemaOwnershipChanges(User user) + { + IEnumerator> enumerator = this.currentState.isSchemaOwned.GetEnumerator(); + enumerator.Reset(); + + String nullString = null; + + while (enumerator.MoveNext()) + { + string schemaName = enumerator.Current.Key.ToString(); + bool userIsOwner = (bool)enumerator.Current.Value; + + if (((bool)this.originalState.isSchemaOwned[schemaName]) != userIsOwner) + { + System.Diagnostics.Debug.Assert(!this.Exists || userIsOwner, "shouldn't have to unset ownership for new users"); + + Schema schema = this.parent.Schemas[schemaName]; + schema.Owner = userIsOwner ? user.Name : nullString; + schema.Alter(); + } + } + } + + private void ApplyRoleMembershipChanges(User user) + { + IEnumerator> enumerator = this.currentState.isMember.GetEnumerator(); + enumerator.Reset(); + + while (enumerator.MoveNext()) + { + string roleName = enumerator.Current.Key; + bool userIsMember = (bool)enumerator.Current.Value; + + if (((bool)this.originalState.isMember[roleName]) != userIsMember) + { + System.Diagnostics.Debug.Assert(this.Exists || userIsMember, "shouldn't have to unset membership for new users"); + + DatabaseRole role = this.parent.Roles[roleName]; + + if (userIsMember) + { + role.AddMember(user.Name); + } + else + { + role.DropMember(user.Name); + } + } + } + } + + protected virtual void SaveProperties(User user) + { + if (!this.Exists || (user.UserType != this.currentState.userType)) + { + user.UserType = this.currentState.userType; + } + + if ((this.currentState.userType == UserType.Certificate) + &&(!this.Exists || (user.Certificate != this.currentState.certificateName)) + ) + { + user.Certificate = this.currentState.certificateName; + } + + if ((this.currentState.userType == UserType.AsymmetricKey) + && (!this.Exists || (user.AsymmetricKey != this.currentState.asymmetricKeyName)) + ) + { + user.AsymmetricKey = this.currentState.asymmetricKeyName; + } + } + + public User GetUser() + { + User result = null; + + // if we think we exist, get the SMO user object + if (this.Exists) + { + result = this.parent.Users[this.originalState.name]; + result.Refresh(); + + System.Diagnostics.Debug.Assert(0 == String.Compare(this.originalState.name, this.currentState.name, StringComparison.Ordinal), "name of existing user has changed"); + if (result == null) + { + throw new Exception(); + } + } + else + { + result = new User(this.parent, this.Name); + } + + return result; + } + + /// + /// Will calling ApplyChanges do anything? + /// + /// True if there are changes to apply, false otherwise + public bool ChangesExist() + { + bool result = + !this.Exists || + !this.originalState.HasSameValueAs(this.currentState); + + return result; + } + } + + internal class UserPrototypeWithDefaultSchema : UserPrototypeNew, + IUserPrototypeWithDefaultSchema + { + private CDataContainer context; + + #region IUserPrototypeWithDefaultSchema Members + + public virtual bool IsDefaultSchemaSupported + { + get + { + //Default Schema was not supported in Shiloh. + return this.context.Server.ConnectionContext.ServerVersion.Major > 8 ; + } + } + + public string DefaultSchema + { + get + { + return this.currentState.defaultSchemaName; + } + set + { + this.currentState.defaultSchemaName = value; + } + } + + #endregion + + /// + /// Constructor + /// + /// The context for the dialog + public UserPrototypeWithDefaultSchema(CDataContainer context, + UserPrototypeDataNew current, + UserPrototypeDataNew original) + : base(context, current, original) + { + this.context = context; + } + + protected override void SaveProperties(User user) + { + base.SaveProperties(user); + + if (this.IsDefaultSchemaSupported + && (!this.Exists || (user.DefaultSchema != this.currentState.defaultSchemaName))) + { + user.DefaultSchema = this.currentState.defaultSchemaName; + } + } + } + + internal class UserPrototypeForSqlUserWithLogin : UserPrototypeWithDefaultSchema, + IUserPrototypeWithMappedLogin + { + + #region IUserPrototypeWithMappedLogin Members + + public string LoginName + { + get + { + return this.currentState.mappedLoginName; + } + set + { + this.currentState.mappedLoginName = value; + } + } + + #endregion + + /// + /// Constructor + /// + /// The context for the dialog + public UserPrototypeForSqlUserWithLogin(CDataContainer context, + UserPrototypeDataNew current, + UserPrototypeDataNew original) + : base(context, current, original) + { + } + + protected override void SaveProperties(User user) + { + base.SaveProperties(user); + + if (!this.Exists || (user.Login != this.currentState.mappedLoginName)) + { + user.Login = this.currentState.mappedLoginName; + } + } + } + + internal class UserPrototypeForWindowsUser : UserPrototypeForSqlUserWithLogin, + IUserPrototypeWithDefaultLanguage + { + private CDataContainer context; + + public override bool IsDefaultSchemaSupported + { + get + { + //Default Schema was not supported before Denali for windows group. + User user = null; + + user = this.GetUser(); + if (this.Exists && user.LoginType == LoginType.WindowsGroup) + { + return SqlMgmtUtils.IsSql11OrLater(this.context.Server.ConnectionContext.ServerVersion); + } + else + { + return base.IsDefaultSchemaSupported; + } + } + } + + #region IUserPrototypeWithDefaultLanguage Members + + public bool IsDefaultLanguageSupported + { + get + { + //Default Language was not supported before Denali. + return SqlMgmtUtils.IsSql11OrLater(this.context.Server.ConnectionContext.ServerVersion); + } + } + + public string DefaultLanguageAlias + { + get + { + return this.currentState.defaultLanguageAlias; + } + set + { + this.currentState.defaultLanguageAlias = value; + } + } + + #endregion + + /// + /// Constructor + /// + /// The context for the dialog + public UserPrototypeForWindowsUser(CDataContainer context, + UserPrototypeDataNew current, + UserPrototypeDataNew original) + : base(context, current, original) + { + this.context = context; + } + + protected override void SaveProperties(User user) + { + base.SaveProperties(user); + + if (this.IsDefaultLanguageSupported) + { + //If this.currentState.defaultLanguageAlias is , we will get defaultLanguageName as string.Empty + string defaultLanguageName = LanguageUtils.GetLanguageNameFromAlias(user.Parent.Parent, + this.currentState.defaultLanguageAlias); + + if (!this.Exists || (user.DefaultLanguage.Name != defaultLanguageName)) //comparing name of the language. + { + //Default language is invalid inside an uncontained database. + if (user.Parent.ContainmentType != ContainmentType.None) + { + //Setting what user has set, i.e. the Alias of the language. + user.DefaultLanguage.Name = this.currentState.defaultLanguageAlias; + } + } + } + } + } + + internal class UserPrototypeForSqlUserWithPassword : UserPrototypeWithDefaultSchema, + IUserPrototypeWithDefaultLanguage, + IUserPrototypeWithPassword + { + private CDataContainer context; + + #region IUserPrototypeWithDefaultLanguage Members + + public bool IsDefaultLanguageSupported + { + get + { + //Default Language was not supported before Denali. + return SqlMgmtUtils.IsSql11OrLater(this.context.Server.ConnectionContext.ServerVersion); + } + } + + public string DefaultLanguageAlias + { + get + { + return this.currentState.defaultLanguageAlias; + } + set + { + this.currentState.defaultLanguageAlias = value; + } + } + + #endregion + + #region IUserPrototypeWithPassword Members + + public SecureString Password + { + set + { + this.currentState.password = value; + } + } + + public SecureString PasswordConfirm + { + set + { + this.currentState.passwordConfirm = value; + } + } + + public SecureString OldPassword + { + set + { + this.currentState.oldPassword = value; + } + } + + public bool IsOldPasswordRequired + { + get + { + return this.currentState.isOldPasswordRequired; + } + set + { + this.currentState.isOldPasswordRequired = value; + } + } + + #endregion + + /// + /// Constructor + /// + /// The context for the dialog + public UserPrototypeForSqlUserWithPassword(CDataContainer context, + UserPrototypeDataNew current, + UserPrototypeDataNew original) + : base(context, current, original) + { + this.context = context; + } + + protected override void SaveProperties(User user) + { + base.SaveProperties(user); + + if (this.IsDefaultLanguageSupported) + { + //If this.currentState.defaultLanguageAlias is , we will get defaultLanguageName as string.Empty + string defaultLanguageName = LanguageUtils.GetLanguageNameFromAlias(user.Parent.Parent, + this.currentState.defaultLanguageAlias); + + if (!this.Exists || (user.DefaultLanguage.Name != defaultLanguageName)) //comparing name of the language. + { + //Default language is invalid inside an uncontained database. + if (user.Parent.ContainmentType != ContainmentType.None) + { + //Setting what user has set, i.e. the Alias of the language. + user.DefaultLanguage.Name = this.currentState.defaultLanguageAlias; + } + } + } + } + + protected override void CreateOrAlterUser(User user) + { + if (!this.Exists) //New User + { + user.Create(this.currentState.password); + } + else //Existing User + { + user.Alter(); + + if (this.currentState.password != this.originalState.password) + { + if (this.currentState.isOldPasswordRequired) + { + user.ChangePassword(this.currentState.oldPassword, this.currentState.password); + } + else + { + user.ChangePassword(this.currentState.password); + } + } + } + } + } + + /// + /// 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 + { + private static UserPrototypeFactory singletonInstance; + + private UserPrototypeDataNew currentData; + private UserPrototypeDataNew originalData; + private CDataContainer context; + + private UserPrototypeNew asymmetricKeyMappedUser; + private UserPrototypeNew certificateMappedUser; + private UserPrototypeNew loginMappedUser; + private UserPrototypeNew noLoginUser; + private UserPrototypeNew sqlUserWithPassword; + private UserPrototypeNew windowsUser; + + private UserPrototypeNew currentPrototype; + + public UserPrototypeNew CurrentPrototype + { + get + { + currentPrototype ??= new UserPrototypeNew(this.context, + this.currentData, + this.originalData); + return currentPrototype; + } + } + + private UserPrototypeFactory(CDataContainer context) + { + this.context = context; + + this.originalData = new UserPrototypeDataNew(this.context); + this.currentData = this.originalData.Clone(); + } + + public static UserPrototypeFactory GetInstance(CDataContainer context) + { + if (singletonInstance != null + && singletonInstance.context != context) + { + singletonInstance = null; + } + + singletonInstance ??= new UserPrototypeFactory(context); + + return singletonInstance; + } + + public UserPrototypeNew GetUserPrototype(ExhaustiveUserTypes userType) + { + switch (userType) + { + case ExhaustiveUserTypes.AsymmetricKeyMappedUser: + currentData.userType = UserType.AsymmetricKey; + this.asymmetricKeyMappedUser ??= new UserPrototypeNew(this.context, this.currentData, this.originalData); + this.currentPrototype = asymmetricKeyMappedUser; + break; + + case ExhaustiveUserTypes.CertificateMappedUser: + currentData.userType = UserType.Certificate; + this.certificateMappedUser ??= new UserPrototypeNew(this.context, this.currentData, this.originalData); + this.currentPrototype = certificateMappedUser; + break; + + case ExhaustiveUserTypes.LoginMappedUser: + currentData.userType = UserType.SqlUser; + this.loginMappedUser ??= new UserPrototypeForSqlUserWithLogin(this.context, this.currentData, this.originalData); + this.currentPrototype = loginMappedUser; + break; + + case ExhaustiveUserTypes.SqlUserWithoutLogin: + currentData.userType = UserType.NoLogin; + this.noLoginUser ??= new UserPrototypeWithDefaultSchema(this.context, this.currentData, this.originalData); + this.currentPrototype = noLoginUser; + break; + + case ExhaustiveUserTypes.SqlUserWithPassword: + currentData.userType = UserType.SqlUser; + this.sqlUserWithPassword ??= new UserPrototypeForSqlUserWithPassword(this.context, this.currentData, this.originalData); + this.currentPrototype = sqlUserWithPassword; + break; + + case ExhaustiveUserTypes.WindowsUser: + currentData.userType = UserType.SqlUser; + this.windowsUser ??= new UserPrototypeForWindowsUser(this.context, this.currentData, this.originalData); + this.currentPrototype = windowsUser; + break; + + default: + System.Diagnostics.Debug.Assert(false, "Unknown UserType provided."); + this.currentPrototype = null; + break; + } + return this.currentPrototype; + } + } + + /// + /// Lists all types of possible database users. + /// + internal enum ExhaustiveUserTypes + { + Unknown, + SqlUserWithoutLogin, + SqlUserWithPassword, + WindowsUser, + LoginMappedUser, + CertificateMappedUser, + AsymmetricKeyMappedUser + }; + + internal class LanguageUtils + { + /// + /// Gets alias for a language name. + /// + /// + /// + /// Returns string.Empty in case it doesn't find a matching languageName on the server + public static string GetLanguageAliasFromName(Server connectedServer, + string languageName) + { + string languageAlias = string.Empty; + + SetLanguageDefaultInitFieldsForDefaultLanguages(connectedServer); + + foreach (Language lang in connectedServer.Languages) + { + if (lang.Name == languageName) + { + languageAlias = lang.Alias; + break; + } + } + + return languageAlias; + } + + /// + /// Gets name for a language alias. + /// + /// + /// + /// Returns string.Empty in case it doesn't find a matching languageAlias on the server + public static string GetLanguageNameFromAlias(Server connectedServer, + string languageAlias) + { + string languageName = string.Empty; + + SetLanguageDefaultInitFieldsForDefaultLanguages(connectedServer); + + foreach (Language lang in connectedServer.Languages) + { + if (lang.Alias == languageAlias) + { + languageName = lang.Name; + break; + } + } + + return languageName; + } + + /// + /// Sets exhaustive fields required for displaying and working with default languages in server, + /// database and user dialogs as default init fields so that queries are not sent again and again. + /// + /// server on which languages will be enumerated + public static void SetLanguageDefaultInitFieldsForDefaultLanguages(Server connectedServer) + { + string[] fieldsNeeded = new string[] { "Alias", "Name", "LocaleID", "LangID" }; + connectedServer.SetDefaultInitFields(typeof(Language), fieldsNeeded); + } + } + + internal class ObjectNoLongerExistsException : Exception + { + private static string ExceptionMessage + { + get + { + return "Object no longer exists"; + } + } + + public ObjectNoLongerExistsException() + : base(ExceptionMessage) + { + // + // TODO: Add constructor logic here + // + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Security/LoginTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Security/LoginTests.cs new file mode 100644 index 00000000..1171561e --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Security/LoginTests.cs @@ -0,0 +1,69 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading.Tasks; +using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; +using Microsoft.SqlTools.ServiceLayer.Security; +using Microsoft.SqlTools.ServiceLayer.Security.Contracts; +using Microsoft.SqlTools.ServiceLayer.Test.Common; +using Microsoft.SqlTools.ServiceLayer.Utility; +using Moq; + +namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Security +{ + /// + /// Tests for the Login management component + /// + public class LoginTests + { + /// + /// Test the basic Create Login method handler + /// + // [Test] + public async Task TestHandleCreateLoginRequest() + { + using (SelfCleaningTempFile queryTempFile = new SelfCleaningTempFile()) + { + // setup + var connectionResult = await LiveConnectionHelper.InitLiveConnectionInfoAsync("master", queryTempFile.FilePath); + var loginParams = new CreateLoginParams + { + OwnerUri = connectionResult.ConnectionInfo.OwnerUri, + Login = SecurityTestUtils.GetTestLoginInfo() + }; + + var createContext = new Mock>(); + createContext.Setup(x => x.SendResult(It.IsAny())) + .Returns(Task.FromResult(new object())); + + // call the create login method + SecurityService service = new SecurityService(); + await service.HandleCreateLoginRequest(loginParams, createContext.Object); + + // verify the result + createContext.Verify(x => x.SendResult(It.Is + (p => p.Success && p.Login.LoginName != string.Empty))); + + // cleanup created login + var deleteParams = new DeleteLoginParams + { + OwnerUri = connectionResult.ConnectionInfo.OwnerUri, + LoginName = loginParams.Login.LoginName + }; + + var deleteContext = new Mock>(); + deleteContext.Setup(x => x.SendResult(It.IsAny())) + .Returns(Task.FromResult(new object())); + + // call the create login method + await service.HandleDeleteLoginRequest(deleteParams, deleteContext.Object); + + // verify the result + deleteContext.Verify(x => x.SendResult(It.Is(p => p.Success))); + } + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Security/SecurityTestUtils.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Security/SecurityTestUtils.cs index 1d1329f4..bfeb906d 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Security/SecurityTestUtils.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Security/SecurityTestUtils.cs @@ -23,6 +23,28 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Security return string.Format(@"{0}\{1}", Environment.UserDomainName, Environment.UserName); } + internal static LoginInfo GetTestLoginInfo() + { + return new LoginInfo() + { + LoginName = "TestLoginName_" + new Random().NextInt64(10000000,90000000).ToString(), + LoginType= LoginType.Sql, + CertificateName = "Test Cert", + AsymmetricKeyName = "Asymmetric Test Cert", + WindowsGrantAccess = true, + MustChange = false, + IsDisabled = false, + IsLockedOut = false, + EnforcePolicy = false, + EnforceExpiration = false, + WindowsAuthSupported = false, + Password = "!#!@#@#@dflksdjfksdlfjlksdFEEfjklsed9393", + OldPassword = "{{OLD_TEST_PASSWORD_PLACEHOLDER}}", + DefaultLanguage = "us_english", + DefaultDatabase = "master" + }; + } + internal static CredentialInfo GetTestCredentialInfo() { return new CredentialInfo() diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Microsoft.SqlTools.ServiceLayer.UnitTests.csproj b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Microsoft.SqlTools.ServiceLayer.UnitTests.csproj index 99a5d237..fa6d2654 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Microsoft.SqlTools.ServiceLayer.UnitTests.csproj +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Microsoft.SqlTools.ServiceLayer.UnitTests.csproj @@ -5,7 +5,7 @@ $(DefineConstants);NETCOREAPP1_0;TRACE false - +