// // 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.Linq; using System.Security; using Microsoft.SqlServer.Management.Common; using Microsoft.SqlServer.Management.Smo; using Microsoft.SqlServer.Management.Sdk.Sfc; using Microsoft.SqlTools.ServiceLayer.Management; using Microsoft.SqlTools.ServiceLayer.Security.Contracts; using Microsoft.SqlTools.ServiceLayer.Utility; 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 UserPrototypeData { public string name = string.Empty; public UserType userType = UserType.SqlUser; public bool isSystemObject = false; public Dictionary isSchemaOwned; public Dictionary isMember; 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 UserPrototypeData() { this.isSchemaOwned = new Dictionary(); this.isMember = new Dictionary(); } public UserPrototypeData(CDataContainer context, UserInfo? userInfo) { this.isSchemaOwned = new Dictionary(); this.isMember = new Dictionary(); // load user properties from SMO object if (!context.IsNewObject) { this.LoadUserData(context); } // apply user properties provided by client if (userInfo != null) { this.name = userInfo.Name; this.mappedLoginName = userInfo.LoginName; this.defaultSchemaName = userInfo.DefaultSchema; if (!string.IsNullOrEmpty(userInfo.Password)) { this.password = DatabaseUtils.GetReadOnlySecureString(userInfo.Password); } if (!string.IsNullOrEmpty(userInfo.DefaultLanguage) && string.Compare(userInfo.DefaultLanguage, SR.DefaultLanguagePlaceholder, StringComparison.Ordinal) != 0) { this.defaultLanguageAlias = LanguageUtils.GetLanguageAliasFromDisplayText(userInfo.DefaultLanguage); } } this.LoadRoleMembership(context, userInfo); this.LoadSchemaData(context, userInfo); } public UserPrototypeData Clone() { UserPrototypeData result = new UserPrototypeData(); 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 ?? Enumerable.Empty()) { result.isMember[key] = this.isMember[key]; } foreach (string key in this.isSchemaOwned?.Keys ?? Enumerable.Empty()) { result.isSchemaOwned[key] = this.isSchemaOwned[key]; } return result; } public bool HasSameValueAs(UserPrototypeData 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 ?? Enumerable.Empty()) { if (this.isMember?.ContainsKey(key) == true && other.isMember?.ContainsKey(key) == true && 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; if (existingUser == null) { return; } 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; if (context.Server.ServerType != DatabaseEngineType.SqlAzureDatabase) { this.defaultLanguageAlias = LanguageUtils.GetLanguageAliasFromName( existingUser.Parent.Parent, existingUser.DefaultLanguage.Name); } } } /// /// Loads role membership of a database user. /// /// private void LoadRoleMembership(CDataContainer context, UserInfo? userInfo) { Urn objUrn = new Urn(context.ObjectUrn); Urn databaseUrn = objUrn.Parent; Database? parentDb = context.Server.GetSmoObject(databaseUrn) as Database; if (parentDb == null) { return; } string userName = userInfo?.Name ?? objUrn.GetNameForType("User"); User existingUser = context.Server.Databases[parentDb.Name].Users[userName]; foreach (DatabaseRole dbRole in parentDb.Roles) { var comparer = parentDb.GetStringComparer(); if (comparer.Compare(dbRole.Name, "public") != 0) { if (userInfo != null && userInfo.DatabaseRoles != null) { this.isMember[dbRole.Name] = userInfo.DatabaseRoles.Contains(dbRole.Name); } else if (existingUser != null) { this.isMember[dbRole.Name] = existingUser.IsMember(dbRole.Name); } else { this.isMember[dbRole.Name] = false; } } } } /// /// Loads schema ownership related data. /// /// private void LoadSchemaData(CDataContainer context, UserInfo? userInfo) { Urn objUrn = new Urn(context.ObjectUrn); Urn databaseUrn = objUrn.Parent; Database? parentDb = context.Server.GetSmoObject(databaseUrn) as Database; if (parentDb == null) { return; } string userName = userInfo?.Name ?? objUrn.GetNameForType("User"); User existingUser = context.Server.Databases[parentDb.Name].Users[userName]; if (!SqlMgmtUtils.IsYukonOrAbove(context.Server) || parentDb.CompatibilityLevel <= CompatibilityLevel.Version80) { return; } foreach (Schema sch in parentDb.Schemas) { if (userInfo != null && userInfo.OwnedSchemas != null) { this.isSchemaOwned[sch.Name] = userInfo.OwnedSchemas.Contains(sch.Name); } else if (existingUser != null) { var comparer = parentDb.GetStringComparer(); this.isSchemaOwned[sch.Name] = comparer.Compare(sch.Owner, existingUser.Name) == 0; } else { this.isSchemaOwned[sch.Name] = false; } } } } /// /// Prototype object for creating or altering users /// internal class UserPrototype : IUserPrototype { protected UserPrototypeData originalState; protected UserPrototypeData currentState; private List schemaNames; private List roleNames; private bool exists = false; private Database parent; public bool IsRoleMembershipChangesApplied { get; set; } //default is false public bool IsSchemaOwnershipChangesApplied { get; set; } //default is false #region IUserPrototype Members public UserPrototypeData CurrentState { get { return this.currentState; } } 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 UserPrototype(CDataContainer context, UserPrototypeData current, UserPrototypeData original) { this.currentState = current; this.originalState = original; this.exists = !context.IsNewObject; Database? parent = context.Server.GetSmoObject(new Urn(context.ParentUrn)) as Database ?? throw new ArgumentException("Context ParentUrn is invalid"); this.parent = parent; this.roleNames = this.PopulateRoles(); this.schemaNames = this.PopulateSchemas(); } private List PopulateRoles() { var roleNames = new List(); foreach (DatabaseRole dbRole in this.parent.Roles) { var comparer = this.parent.GetStringComparer(); if (comparer.Compare(dbRole.Name, "public") != 0) { roleNames.Add(dbRole.Name); } } return roleNames; } private List PopulateSchemas() { var schemaNames = new List(); if (!SqlMgmtUtils.IsYukonOrAbove(this.parent.Parent) || this.parent.CompatibilityLevel <= CompatibilityLevel.Version80) { throw new ArgumentException("Unsupported server version"); } foreach (Schema sch in this.parent.Schemas) { schemaNames.Add(sch.Name); } return schemaNames; } public bool IsYukonOrLater { get { return SqlMgmtUtils.IsYukonOrAbove(this.parent.Parent); } } public User ApplyChanges() { User 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(); if (enumerator != null) { enumerator.Reset(); string? nullString = null; while (enumerator.MoveNext()) { string schemaName = enumerator.Current.Key.ToString(); bool userIsOwner = (bool)enumerator.Current.Value; if (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(); if (enumerator != null) { enumerator.Reset(); while (enumerator.MoveNext()) { string roleName = enumerator.Current.Key; bool userIsMember = enumerator.Current.Value; if (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; // 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 : UserPrototype, 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, UserPrototypeData current, UserPrototypeData 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, UserPrototypeData current, UserPrototypeData original) : base(context, current, original) { } protected override void SaveProperties(User user) { base.SaveProperties(user); bool isValidLoginName = !string.IsNullOrWhiteSpace(this.currentState.mappedLoginName); bool isCreatingOrUpdatingLogin = !this.Exists || user.Login != this.currentState.mappedLoginName; if (isValidLoginName && isCreatingOrUpdatingLogin) { user.Login = this.currentState.mappedLoginName; } } } internal class UserPrototypeForWindowsUser : UserPrototypeForSqlUserWithLogin, IUserPrototypeWithDefaultLanguage { private CDataContainer context; public override bool IsDefaultSchemaSupported { get { //Default Schema was not supported before Denali for windows group. User user = this.GetUser(); if (this.Exists && user.LoginType == Microsoft.SqlServer.Management.Smo.LoginType.WindowsGroup) { return SqlMgmtUtils.IsSql11OrLater(this.context.Server.ConnectionContext.ServerVersion); } else { return base.IsDefaultSchemaSupported; } } } #region IUserPrototypeWithDefaultLanguage Members public bool IsDefaultLanguageSupported { get { return LanguageUtils.IsDefaultLanguageSupported(this.context.Server); } } public string DefaultLanguageAlias { get { return this.currentState.defaultLanguageAlias; } set { this.currentState.defaultLanguageAlias = value; } } #endregion /// /// Constructor /// /// The context for the dialog public UserPrototypeForWindowsUser(CDataContainer context, UserPrototypeData current, UserPrototypeData 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 or on SQL DB. bool isSqlAzure = this.context.ServerConnection.DatabaseEngineType == DatabaseEngineType.SqlAzureDatabase; return !isSqlAzure && 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, UserPrototypeData current, UserPrototypeData 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 (!DatabaseUtils.IsSecureStringsEqual(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. /// internal static class UserPrototypeFactory { public static UserPrototype GetUserPrototype( CDataContainer context, UserInfo? user, UserPrototypeData? originalData, ExhaustiveUserTypes userType) { UserPrototype currentPrototype = null; UserPrototypeData currentData = new UserPrototypeData(context, user); switch (userType) { case ExhaustiveUserTypes.AsymmetricKeyMappedUser: currentPrototype ??= new UserPrototype(context, currentData, originalData); break; case ExhaustiveUserTypes.CertificateMappedUser: currentPrototype ??= new UserPrototype(context, currentData, originalData); break; case ExhaustiveUserTypes.LoginMappedUser: currentPrototype ??= new UserPrototypeForSqlUserWithLogin(context, currentData, originalData); break; case ExhaustiveUserTypes.SqlUserWithoutLogin: currentPrototype ??= new UserPrototypeWithDefaultSchema(context, currentData, originalData); break; case ExhaustiveUserTypes.SqlUserWithPassword: currentPrototype ??= new UserPrototypeForSqlUserWithPassword(context, currentData, originalData); break; case ExhaustiveUserTypes.WindowsUser: currentPrototype ??= new UserPrototypeForWindowsUser(context, currentData, originalData); break; default: System.Diagnostics.Debug.Assert(false, "Unknown UserType provided."); currentPrototype = null; break; } return currentPrototype; } } /// /// Lists all types of possible database users. /// internal enum ExhaustiveUserTypes { Unknown, SqlUserWithoutLogin, SqlUserWithPassword, WindowsUser, LoginMappedUser, CertificateMappedUser, AsymmetricKeyMappedUser }; }