diff --git a/src/Umbraco.Core/Models/Membership/UmbracoMembershipUser.cs b/src/Umbraco.Core/Models/Membership/UmbracoMembershipUser.cs new file mode 100644 index 0000000000..e45cec04c2 --- /dev/null +++ b/src/Umbraco.Core/Models/Membership/UmbracoMembershipUser.cs @@ -0,0 +1,50 @@ +using System; +using System.Web.Security; + +namespace Umbraco.Core.Models.Membership +{ + internal class UmbracoMembershipUser : MembershipUser where T : IMembershipUser + { + private T _user; + + #region Constructors + /// + /// Initializes a new instance of the class. + /// + public UmbracoMembershipUser(T user) + { + _user = user; + } + + /// + /// Initializes a new instance of the class. + /// + /// Name of the provider. + /// The name. + /// The provider user key. + /// The email. + /// The password question. + /// The comment. + /// if set to true [is approved]. + /// if set to true [is locked out]. + /// The creation date. + /// The last login date. + /// The last activity date. + /// The last password changed date. + /// The last lockout date. + /// The full name. + /// The language. + /// + public UmbracoMembershipUser(string providerName, string name, object providerUserKey, string email, + string passwordQuestion, string comment, bool isApproved, bool isLockedOut, + DateTime creationDate, DateTime lastLoginDate, DateTime lastActivityDate, DateTime lastPasswordChangedDate, + DateTime lastLockoutDate, string fullName, string language, T user) + : base(providerName, name, providerUserKey, email, passwordQuestion, comment, isApproved, isLockedOut, + creationDate, lastLoginDate, lastActivityDate, lastPasswordChangedDate, lastLockoutDate) + { + _user = user; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Membership/UserGroup.cs b/src/Umbraco.Core/Models/Membership/UserGroup.cs new file mode 100644 index 0000000000..6b20b07110 --- /dev/null +++ b/src/Umbraco.Core/Models/Membership/UserGroup.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.Serialization; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Strings; + +namespace Umbraco.Core.Models.Membership +{ + /// + /// Represents a Group for a Backoffice User + /// + [Serializable] + [DataContract(IsReference = true)] + internal class UserGroup : Entity, IUserGroup, IReadOnlyUserGroup + { + private int? _startContentId; + private int? _startMediaId; + private string _alias; + private string _icon; + private string _name; + private IEnumerable _permissions; + private readonly List _sectionCollection; + + private static readonly Lazy Ps = new Lazy(); + + // ReSharper disable once ClassNeverInstantiated.Local // lazy-instanciated in Ps + private class PropertySelectors + { + public readonly PropertyInfo NameSelector = ExpressionHelper.GetPropertyInfo(x => x.Name); + public readonly PropertyInfo AliasSelector = ExpressionHelper.GetPropertyInfo(x => x.Alias); + public readonly PropertyInfo PermissionsSelector = ExpressionHelper.GetPropertyInfo>(x => x.Permissions); + public readonly PropertyInfo IconSelector = ExpressionHelper.GetPropertyInfo(x => x.Icon); + public readonly PropertyInfo StartContentIdSelector = ExpressionHelper.GetPropertyInfo(x => x.StartContentId); + public readonly PropertyInfo StartMediaIdSelector = ExpressionHelper.GetPropertyInfo(x => x.StartMediaId); + + //Custom comparer for enumerable + public readonly DelegateEqualityComparer> StringEnumerableComparer = + new DelegateEqualityComparer>( + (enum1, enum2) => enum1.UnsortedSequenceEqual(enum2), + enum1 => enum1.GetHashCode()); + } + + /// + /// Constructor to create a new user group + /// + public UserGroup() + { + _sectionCollection = new List(); + } + + /// + /// Constructor to create an existing user group + /// + /// + /// + /// + /// + /// + public UserGroup(int userCount, string alias, string name, IEnumerable permissions, string icon) + : this() + { + UserCount = userCount; + _alias = alias; + _name = name; + _permissions = permissions; + _icon = icon; + } + + [DataMember] + public int? StartMediaId + { + get => _startMediaId; + set => SetPropertyValueAndDetectChanges(value, ref _startMediaId, Ps.Value.StartMediaIdSelector); + } + + [DataMember] + public int? StartContentId + { + get => _startContentId; + set => SetPropertyValueAndDetectChanges(value, ref _startContentId, Ps.Value.StartContentIdSelector); + } + + [DataMember] + public string Icon + { + get => _icon; + set => SetPropertyValueAndDetectChanges(value, ref _icon, Ps.Value.IconSelector); + } + + [DataMember] + public string Alias + { + get => _alias; + set => SetPropertyValueAndDetectChanges(value.ToCleanString(CleanStringType.Alias | CleanStringType.UmbracoCase), ref _alias, Ps.Value.AliasSelector); + } + + [DataMember] + public string Name + { + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name, Ps.Value.NameSelector); + } + + /// + /// The set of default permissions for the user group + /// + /// + /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more flexible permissions structure in the future. + /// + [DataMember] + public IEnumerable Permissions + { + get => _permissions; + set => SetPropertyValueAndDetectChanges(value, ref _permissions, Ps.Value.PermissionsSelector, Ps.Value.StringEnumerableComparer); + } + + public IEnumerable AllowedSections => _sectionCollection; + + public void RemoveAllowedSection(string sectionAlias) + { + if (_sectionCollection.Contains(sectionAlias)) + _sectionCollection.Remove(sectionAlias); + } + + public void AddAllowedSection(string sectionAlias) + { + if (_sectionCollection.Contains(sectionAlias) == false) + _sectionCollection.Add(sectionAlias); + } + + public void ClearAllowedSections() + { + _sectionCollection.Clear(); + } + + public int UserCount { get; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Packaging/PackageAction.cs b/src/Umbraco.Core/Models/Packaging/PackageAction.cs index 7ad86977a6..dd9a4fd1a5 100644 --- a/src/Umbraco.Core/Models/Packaging/PackageAction.cs +++ b/src/Umbraco.Core/Models/Packaging/PackageAction.cs @@ -23,14 +23,14 @@ namespace Umbraco.Core.Models.Packaging public ActionRunAt RunAt { - get { return _runAt == ActionRunAt.Undefined ? ActionRunAt.Install : _runAt; } - set { _runAt = value; } + get => _runAt == ActionRunAt.Undefined ? ActionRunAt.Install : _runAt; + set => _runAt = value; } public bool Undo //NOTE: Should thid default to "False"? but the documentation says default "True" (http://our.umbraco.org/wiki/reference/packaging/package-actions) { - get { return _undo ?? true; } - set { _undo = value; } + get => _undo ?? true; + set => _undo = value; } public XElement XmlData { get; set; } diff --git a/src/Umbraco.Core/Models/Rdbms/DictionaryDto.cs b/src/Umbraco.Core/Models/Rdbms/DictionaryDto.cs index 8b4a078f35..ffb7e3cee3 100644 --- a/src/Umbraco.Core/Models/Rdbms/DictionaryDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/DictionaryDto.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using NPoco; -using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.DatabaseAnnotations; namespace Umbraco.Core.Models.Rdbms diff --git a/src/Umbraco.Core/Models/Rdbms/User2UserGroupDto.cs b/src/Umbraco.Core/Models/Rdbms/User2UserGroupDto.cs index afdbf661b5..65b48b628a 100644 --- a/src/Umbraco.Core/Models/Rdbms/User2UserGroupDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/User2UserGroupDto.cs @@ -1,4 +1,4 @@ -using Umbraco.Core.Persistence; +using NPoco; using Umbraco.Core.Persistence.DatabaseAnnotations; namespace Umbraco.Core.Models.Rdbms diff --git a/src/Umbraco.Core/Models/Rdbms/UserDto.cs b/src/Umbraco.Core/Models/Rdbms/UserDto.cs index 38e4344f1e..669cb91908 100644 --- a/src/Umbraco.Core/Models/Rdbms/UserDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserDto.cs @@ -29,7 +29,7 @@ namespace Umbraco.Core.Models.Rdbms [Column("userNoConsole")] [Constraint(Default = "0")] public bool NoConsole { get; set; } - + [Column("userName")] public string UserName { get; set; } @@ -108,8 +108,8 @@ namespace Umbraco.Core.Models.Rdbms [Reference(ReferenceType.Many, ReferenceMemberName = "UserId")] public List UserGroupDtos { get; set; } - [ResultColumn] - // fixme - reference? + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = "UserId")] public HashSet UserStartNodeDtos { get; set; } } } diff --git a/src/Umbraco.Core/Models/Rdbms/UserGroup2AppDto.cs b/src/Umbraco.Core/Models/Rdbms/UserGroup2AppDto.cs index 14fc60e197..13a0a4473d 100644 --- a/src/Umbraco.Core/Models/Rdbms/UserGroup2AppDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserGroup2AppDto.cs @@ -1,4 +1,4 @@ -using Umbraco.Core.Persistence; +using NPoco; using Umbraco.Core.Persistence.DatabaseAnnotations; namespace Umbraco.Core.Models.Rdbms diff --git a/src/Umbraco.Core/Models/Rdbms/UserGroup2NodePermissionDto.cs b/src/Umbraco.Core/Models/Rdbms/UserGroup2NodePermissionDto.cs index 8c75dcf4f7..c4782e7259 100644 --- a/src/Umbraco.Core/Models/Rdbms/UserGroup2NodePermissionDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserGroup2NodePermissionDto.cs @@ -1,4 +1,4 @@ -using Umbraco.Core.Persistence; +using NPoco; using Umbraco.Core.Persistence.DatabaseAnnotations; namespace Umbraco.Core.Models.Rdbms diff --git a/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs b/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs index fdad78bfcd..c52a4542b0 100644 --- a/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Umbraco.Core.Persistence; +using NPoco; using Umbraco.Core.Persistence.DatabaseAnnotations; using Umbraco.Core.Persistence.DatabaseModelDefinitions; diff --git a/src/Umbraco.Core/Models/Rdbms/UserStartNodeDto.cs b/src/Umbraco.Core/Models/Rdbms/UserStartNodeDto.cs index fa8af80759..ecc360c24e 100644 --- a/src/Umbraco.Core/Models/Rdbms/UserStartNodeDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserStartNodeDto.cs @@ -1,11 +1,11 @@ using System; -using Umbraco.Core.Persistence; +using NPoco; using Umbraco.Core.Persistence.DatabaseAnnotations; namespace Umbraco.Core.Models.Rdbms { [TableName("umbracoUserStartNode")] - [PrimaryKey("id", autoIncrement = true)] + [PrimaryKey("id", AutoIncrement = true)] [ExplicitColumns] internal class UserStartNodeDto : IEquatable { @@ -45,7 +45,7 @@ namespace Umbraco.Core.Models.Rdbms { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; + if (obj.GetType() != GetType()) return false; return Equals((UserStartNodeDto) obj); } diff --git a/src/Umbraco.Core/Models/Rdbms/UserTypeDto.cs b/src/Umbraco.Core/Models/Rdbms/UserTypeDto.cs index 1114ead34f..ab5cccb241 100644 --- a/src/Umbraco.Core/Models/Rdbms/UserTypeDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserTypeDto.cs @@ -1,10 +1,10 @@ using System; using NPoco; -using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.DatabaseAnnotations; namespace Umbraco.Core.Models.Rdbms -{ +{ + // fixme - remove in v8 [Obsolete("Table no longer exists as of 7.6 - retained only to support migrations from previous versions")] [TableName("umbracoUserType")] [PrimaryKey("id")] diff --git a/src/Umbraco.Core/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs b/src/Umbraco.Core/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs index 819fa87a56..25bf983d29 100644 --- a/src/Umbraco.Core/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs +++ b/src/Umbraco.Core/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs @@ -1,4 +1,5 @@ -using System.Configuration; +using System; +using System.Configuration; using System.DirectoryServices.AccountManagement; using System.Threading.Tasks; using Umbraco.Core.Models.Identity; @@ -7,8 +8,10 @@ namespace Umbraco.Core.Security { public class ActiveDirectoryBackOfficeUserPasswordChecker : IBackOfficeUserPasswordChecker { - public virtual string ActiveDirectoryDomain { - get { + public virtual string ActiveDirectoryDomain + { + get + { return ConfigurationManager.AppSettings["ActiveDirectoryDomain"]; } } @@ -18,7 +21,13 @@ namespace Umbraco.Core.Security bool isValid; using (var pc = new PrincipalContext(ContextType.Domain, ActiveDirectoryDomain)) { - isValid = pc.ValidateCredentials(user.UserName, password); + isValid = pc.ValidateCredentials(user.UserName, password); + } + + if (isValid && user.HasIdentity == false) + { + //TODO: the user will need to be created locally (i.e. auto-linked) + throw new NotImplementedException("The user " + user.UserName + " does not exist locally and currently the " + typeof(ActiveDirectoryBackOfficeUserPasswordChecker) + " doesn't support auto-linking, see http://issues.umbraco.org/issue/U4-10181"); } var result = isValid diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index 5d02194c29..294b8bbd73 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -201,7 +201,7 @@ namespace Umbraco.Core.Security public static bool RenewUmbracoAuthTicket(this HttpContextBase http) { if (http == null) throw new ArgumentNullException("http"); - http.Items["umbraco-force-auth"] = true; + http.Items[Constants.Security.ForceReAuthFlag] = true; return true; } @@ -213,7 +213,7 @@ namespace Umbraco.Core.Security internal static bool RenewUmbracoAuthTicket(this HttpContext http) { if (http == null) throw new ArgumentNullException("http"); - http.Items["umbraco-force-auth"] = true; + http.Items[Constants.Security.ForceReAuthFlag] = true; return true; } diff --git a/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs index fb4d2d0860..0beba534d9 100644 --- a/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs +++ b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs @@ -33,10 +33,10 @@ namespace Umbraco.Core.Security Username = user.UserName, RealName = user.Name, AllowedApplications = user.AllowedSections, - Culture = user.Culture, + Culture = user.Culture, Roles = user.Roles.Select(x => x.RoleId).ToArray(), - StartContentNode = user.StartContentId, - StartMediaNode = user.StartMediaId, + StartContentNodes = user.CalculatedContentStartNodeIds, + StartMediaNodes = user.CalculatedMediaStartNodeIds, SessionId = user.SecurityStamp }); diff --git a/src/Umbraco.Core/Security/BackOfficeSignInManager.cs b/src/Umbraco.Core/Security/BackOfficeSignInManager.cs index c136bc6a10..3606b2fbf7 100644 --- a/src/Umbraco.Core/Security/BackOfficeSignInManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeSignInManager.cs @@ -101,21 +101,28 @@ namespace Umbraco.Core.Security { return SignInStatus.Failure; } + var user = await UserManager.FindByNameAsync(userName); - if (user == null) - { - return SignInStatus.Failure; - } - if (await UserManager.IsLockedOutAsync(user.Id)) - { - return SignInStatus.LockedOut; - } + + //if the user is null, create an empty one which can be used for auto-linking + if (user == null) + user = BackOfficeIdentityUser.CreateNew(userName, null, GlobalSettings.DefaultUILanguage); + + //check the password for the user, this will allow a developer to auto-link + //an account if they have specified an IBackOfficeUserPasswordChecker if (await UserManager.CheckPasswordAsync(user, password)) { + //the underlying call to this will query the user by Id which IS cached! + if (await UserManager.IsLockedOutAsync(user.Id)) + { + return SignInStatus.LockedOut; + } + await UserManager.ResetAccessFailedCountAsync(user.Id); return await SignInOrTwoFactor(user, isPersistent); } - if (shouldLockout) + + if (user.HasIdentity && shouldLockout) { // If lockout is requested, increment access failed count which might lock out the user await UserManager.AccessFailedAsync(user.Id); @@ -125,7 +132,7 @@ namespace Umbraco.Core.Security } } return SignInStatus.Failure; - } + } /// /// Borrowed from Micorosoft's underlying sign in manager which is not flexible enough to tell it to use a different cookie type diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index 148de877de..265e49300d 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -41,6 +41,7 @@ namespace Umbraco.Core.Security /// /// /// + /// /// /// /// @@ -48,6 +49,7 @@ namespace Umbraco.Core.Security IdentityFactoryOptions options, IUserService userService, IMemberTypeService memberTypeService, + IEntityService entityService, IExternalLoginService externalLoginService, MembershipProviderBase membershipProvider) { @@ -56,7 +58,7 @@ namespace Umbraco.Core.Security if (memberTypeService == null) throw new ArgumentNullException("memberTypeService"); if (externalLoginService == null) throw new ArgumentNullException("externalLoginService"); - var manager = new BackOfficeUserManager(new BackOfficeUserStore(userService, memberTypeService, externalLoginService, membershipProvider)); + var manager = new BackOfficeUserManager(new BackOfficeUserStore(userService, memberTypeService, entityService, externalLoginService, membershipProvider)); manager.InitUserManager(manager, membershipProvider, options); return manager; } @@ -93,7 +95,6 @@ namespace Umbraco.Core.Security //NOTE: This method is mostly here for backwards compat base.InitUserManager(manager, membershipProvider, options.DataProtectionProvider); } - } /// @@ -107,6 +108,7 @@ namespace Umbraco.Core.Security { } + #region What we support do not currently //NOTE: Not sure if we really want/need to ever support this @@ -140,7 +142,9 @@ namespace Umbraco.Core.Security /// Initializes the user manager with the correct options /// /// - /// + /// + /// The for the users called UsersMembershipProvider + /// /// /// protected void InitUserManager( @@ -149,25 +153,17 @@ namespace Umbraco.Core.Security IDataProtectionProvider dataProtectionProvider) { // Configure validation logic for usernames - manager.UserValidator = new UserValidator(manager) + manager.UserValidator = new BackOfficeUserValidator(manager) { AllowOnlyAlphanumericUserNames = false, RequireUniqueEmail = true }; // Configure validation logic for passwords - manager.PasswordValidator = new PasswordValidator - { - RequiredLength = membershipProvider.MinRequiredPasswordLength, - RequireNonLetterOrDigit = membershipProvider.MinRequiredNonAlphanumericCharacters > 0, - RequireDigit = false, - RequireLowercase = false, - RequireUppercase = false - //TODO: Do we support the old regex match thing that membership providers used? - }; + manager.PasswordValidator = new MembershipProviderPasswordValidator(membershipProvider); //use a custom hasher based on our membership provider - manager.PasswordHasher = new MembershipPasswordHasher(membershipProvider); + manager.PasswordHasher = GetDefaultPasswordHasher(membershipProvider); if (dataProtectionProvider != null) { @@ -203,6 +199,76 @@ namespace Umbraco.Core.Security //manager.SmsService = new SmsService(); } + /// + /// This will determine which password hasher to use based on what is defined in config + /// + /// + protected virtual IPasswordHasher GetDefaultPasswordHasher(MembershipProviderBase provider) + { + //if the current user membership provider is unkown (this would be rare), then return the default password hasher + if (provider.IsUmbracoUsersProvider() == false) + return new PasswordHasher(); + + //if the configured provider has legacy features enabled, then return the membership provider password hasher + if (provider.AllowManuallyChangingPassword || provider.DefaultUseLegacyEncoding) + return new MembershipProviderPasswordHasher(provider); + + //we can use the user aware password hasher (which will be the default and preferred way) + return new UserAwareMembershipProviderPasswordHasher(provider); + } + + /// + /// Gets/sets the default back office user password checker + /// + public IBackOfficeUserPasswordChecker BackOfficeUserPasswordChecker { get; set; } + + /// + /// Helper method to generate a password for a user based on the current password validator + /// + /// + public string GeneratePassword() + { + var passwordValidator = PasswordValidator as PasswordValidator; + + if (passwordValidator == null) + { + var membershipPasswordHasher = PasswordHasher as IMembershipProviderPasswordHasher; + + //get the real password validator, this should not be null but in some very rare cases it could be, in which case + //we need to create a default password validator to use since we have no idea what it actually is or what it's rules are + //this is an Edge Case! + passwordValidator = PasswordValidator as PasswordValidator + ?? (membershipPasswordHasher != null + ? new MembershipProviderPasswordValidator(membershipPasswordHasher.MembershipProvider) + : new PasswordValidator()); + } + + var password = Membership.GeneratePassword( + passwordValidator.RequiredLength, + passwordValidator.RequireNonLetterOrDigit ? 2 : 0); + + var random = new Random(); + + var passwordChars = password.ToCharArray(); + + if (passwordValidator.RequireDigit && passwordChars.ContainsAny(Enumerable.Range(48, 58).Select(x => (char)x))) + password += Convert.ToChar(random.Next(48, 58)); // 0-9 + + if (passwordValidator.RequireLowercase && passwordChars.ContainsAny(Enumerable.Range(97, 123).Select(x => (char)x))) + password += Convert.ToChar(random.Next(97, 123)); // a-z + + if (passwordValidator.RequireUppercase && passwordChars.ContainsAny(Enumerable.Range(65, 91).Select(x => (char)x))) + password += Convert.ToChar(random.Next(65, 91)); // A-Z + + if (passwordValidator.RequireNonLetterOrDigit && passwordChars.ContainsAny(Enumerable.Range(33, 48).Select(x => (char)x))) + password += Convert.ToChar(random.Next(33, 48)); // symbols !"#$%&'()*+,-./ + + return password; + } + + + #region Overrides for password logic + /// /// Logic used to validate a username and password /// @@ -228,19 +294,109 @@ namespace Umbraco.Core.Security if (BackOfficeUserPasswordChecker != null) { var result = await BackOfficeUserPasswordChecker.CheckPasswordAsync(user, password); + + if (user.HasIdentity == false) + { + return false; + } + //if the result indicates to not fallback to the default, then return true if the credentials are valid if (result != BackOfficeUserPasswordCheckerResult.FallbackToDefaultChecker) { return result == BackOfficeUserPasswordCheckerResult.ValidCredentials; } } + + //we cannot proceed if the user passed in does not have an identity + if (user.HasIdentity == false) + return false; + //use the default behavior return await base.CheckPasswordAsync(user, password); } + public override Task ChangePasswordAsync(int userId, string currentPassword, string newPassword) + { + return base.ChangePasswordAsync(userId, currentPassword, newPassword); + } + /// - /// Gets/sets the default back office user password checker + /// Override to determine how to hash the password /// - public IBackOfficeUserPasswordChecker BackOfficeUserPasswordChecker { get; set; } + /// + /// + /// + /// + protected override async Task VerifyPasswordAsync(IUserPasswordStore store, T user, string password) + { + var userAwarePasswordHasher = PasswordHasher as IUserAwarePasswordHasher; + if (userAwarePasswordHasher == null) + return await base.VerifyPasswordAsync(store, user, password); + + var hash = await store.GetPasswordHashAsync(user); + return userAwarePasswordHasher.VerifyHashedPassword(user, hash, password) != PasswordVerificationResult.Failed; + } + + /// + /// Override to determine how to hash the password + /// + /// + /// + /// + /// + /// + /// This method is called anytime the password needs to be hashed for storage (i.e. including when reset password is used) + /// + protected override async Task UpdatePassword(IUserPasswordStore passwordStore, T user, string newPassword) + { + var userAwarePasswordHasher = PasswordHasher as IUserAwarePasswordHasher; + if (userAwarePasswordHasher == null) + return await base.UpdatePassword(passwordStore, user, newPassword); + + var result = await PasswordValidator.ValidateAsync(newPassword); + if (result.Succeeded == false) + return result; + + await passwordStore.SetPasswordHashAsync(user, userAwarePasswordHasher.HashPassword(user, newPassword)); + await UpdateSecurityStampInternal(user); + return IdentityResult.Success; + + + } + + /// + /// This is copied from the underlying .NET base class since they decied to not expose it + /// + /// + /// + private async Task UpdateSecurityStampInternal(BackOfficeIdentityUser user) + { + if (SupportsUserSecurityStamp == false) + return; + await GetSecurityStore().SetSecurityStampAsync(user, NewSecurityStamp()); + } + + /// + /// This is copied from the underlying .NET base class since they decied to not expose it + /// + /// + private IUserSecurityStampStore GetSecurityStore() + { + var store = Store as IUserSecurityStampStore; + if (store == null) + throw new NotSupportedException("The current user store does not implement " + typeof(IUserSecurityStampStore<>)); + return store; + } + + /// + /// This is copied from the underlying .NET base class since they decied to not expose it + /// + /// + private static string NewSecurityStamp() + { + return Guid.NewGuid().ToString(); + } + + #endregion } } diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index 0c1dd11447..75a656ee49 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -8,9 +8,13 @@ using System.Web.Security; using AutoMapper; using Microsoft.AspNet.Identity; using Microsoft.Owin; +using Umbraco.Core.Models; +using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Identity; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; +using IUser = Umbraco.Core.Models.Membership.IUser; +using Task = System.Threading.Tasks.Task; namespace Umbraco.Core.Security { @@ -31,13 +35,15 @@ namespace Umbraco.Core.Security { private readonly IUserService _userService; private readonly IMemberTypeService _memberTypeService; + private readonly IEntityService _entityService; private readonly IExternalLoginService _externalLoginService; private bool _disposed = false; - public BackOfficeUserStore(IUserService userService, IMemberTypeService memberTypeService, IExternalLoginService externalLoginService, MembershipProviderBase usersMembershipProvider) + public BackOfficeUserStore(IUserService userService, IMemberTypeService memberTypeService, IEntityService entityService, IExternalLoginService externalLoginService, MembershipProviderBase usersMembershipProvider) { _userService = userService; _memberTypeService = memberTypeService; + _entityService = entityService; _externalLoginService = externalLoginService; if (userService == null) throw new ArgumentNullException("userService"); if (usersMembershipProvider == null) throw new ArgumentNullException("usersMembershipProvider"); @@ -70,41 +76,33 @@ namespace Umbraco.Core.Security ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); - var userType = _userService.GetUserTypeByAlias( - user.UserTypeAlias.IsNullOrWhiteSpace() ? _memberTypeService.GetDefault() : user.UserTypeAlias); + //the password must be 'something' it could be empty if authenticating + // with an external provider so we'll just generate one and prefix it, the + // prefix will help us determine if the password hasn't actually been specified yet. + //this will hash the guid with a salt so should be nicely random + var aspHasher = new PasswordHasher(); + var emptyPasswordValue = Constants.Security.EmptyPasswordPrefix + + aspHasher.HashPassword(Guid.NewGuid().ToString("N")); - var member = new User(userType) + var userEntity = new User(user.Name, user.Email, user.UserName, emptyPasswordValue) { DefaultToLiveEditing = false, - Email = user.Email, Language = user.Culture ?? Configuration.GlobalSettings.DefaultUILanguage, - Name = user.Name, - Username = user.UserName, - StartContentId = user.StartContentId == 0 ? -1 : user.StartContentId, - StartMediaId = user.StartMediaId == 0 ? -1 : user.StartMediaId, + StartContentIds = user.StartContentIds ?? new int[] { }, + StartMediaIds = user.StartMediaIds ?? new int[] { }, IsLockedOut = user.IsLockedOut, - IsApproved = true }; - UpdateMemberProperties(member, user); + UpdateMemberProperties(userEntity, user); + + //TODO: We should deal with Roles --> User Groups here which we currently are not doing - //the password must be 'something' it could be empty if authenticating - // with an external provider so we'll just generate one and prefix it, the - // prefix will help us determine if the password hasn't actually been specified yet. - if (member.RawPasswordValue.IsNullOrWhiteSpace()) - { - //this will hash the guid with a salt so should be nicely random - var aspHasher = new PasswordHasher(); - member.RawPasswordValue = "___UIDEMPTYPWORD__" + - aspHasher.HashPassword(Guid.NewGuid().ToString("N")); + _userService.Save(userEntity); - } - _userService.Save(member); - - if (member.Id == 0) throw new DataException("Could not create the user, check logs for details"); + if (userEntity.Id == 0) throw new DataException("Could not create the user, check logs for details"); //re-assign id - user.Id = member.Id; + user.Id = userEntity.Id; return Task.FromResult(0); } @@ -133,7 +131,7 @@ namespace Umbraco.Core.Security _userService.Save(found); } - if (user.LoginsChanged) + if (user.IsPropertyDirty("Logins")) { var logins = await GetLoginsAsync(user); _externalLoginService.SaveUserLogins(found.Id, logins); @@ -201,7 +199,7 @@ namespace Umbraco.Core.Security return await Task.FromResult(result); } - + /// /// Set the user password hash /// @@ -211,7 +209,7 @@ namespace Umbraco.Core.Security { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); - if (passwordHash.IsNullOrWhiteSpace()) throw new ArgumentNullException("passwordHash"); + if (string.IsNullOrEmpty(passwordHash)) throw new ArgumentNullOrEmptyException(nameof(passwordHash)); user.PasswordHash = passwordHash; @@ -227,7 +225,7 @@ namespace Umbraco.Core.Security { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); - + return Task.FromResult(user.PasswordHash); } @@ -241,7 +239,7 @@ namespace Umbraco.Core.Security ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); - return Task.FromResult(user.PasswordHash.IsNullOrWhiteSpace() == false); + return Task.FromResult(string.IsNullOrEmpty(user.PasswordHash) == false); } /// @@ -281,7 +279,9 @@ namespace Umbraco.Core.Security public Task GetEmailConfirmedAsync(BackOfficeIdentityUser user) { ThrowIfDisposed(); - throw new NotImplementedException(); + if (user == null) throw new ArgumentNullException(nameof(user)); + + return Task.FromResult(user.EmailConfirmed); } /// @@ -292,7 +292,8 @@ namespace Umbraco.Core.Security public Task SetEmailConfirmedAsync(BackOfficeIdentityUser user, bool confirmed) { ThrowIfDisposed(); - throw new NotImplementedException(); + user.EmailConfirmed = confirmed; + return Task.FromResult(0); } /// @@ -376,12 +377,17 @@ namespace Umbraco.Core.Security var result = _externalLoginService.Find(login).ToArray(); if (result.Any()) { - //return the first member that matches the result - var output = (from l in result - select _userService.GetUserById(l.UserId) - into user - where user != null - select Mapper.Map(user)).FirstOrDefault(); + //return the first user that matches the result + BackOfficeIdentityUser output = null; + foreach (var l in result) + { + var user = _userService.GetUserById(l.UserId); + if (user != null) + { + output = Mapper.Map(user); + break; + } + } return Task.FromResult(AssignLoginsCallback(output)); } @@ -391,7 +397,7 @@ namespace Umbraco.Core.Security /// - /// Adds a user to a role (section) + /// Adds a user to a role (user group) /// /// /// @@ -399,27 +405,20 @@ namespace Umbraco.Core.Security { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); + if (string.IsNullOrWhiteSpace(roleName)) throw new ArgumentException("Value cannot be null or whitespace.", "roleName"); - if (user.AllowedSections.InvariantContains(roleName)) return Task.FromResult(0); + var userRole = user.Roles.SingleOrDefault(r => r.RoleId == roleName); - var asInt = user.Id.TryConvertTo(); - if (asInt == false) + if (userRole == null) { - throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); - } - - var found = _userService.GetUserById(asInt.Result); - - if (found != null) - { - found.AddAllowedSection(roleName); + user.AddRole(roleName); } return Task.FromResult(0); } /// - /// Removes the role (allowed section) for the user + /// Removes the role (user group) for the user /// /// /// @@ -427,27 +426,20 @@ namespace Umbraco.Core.Security { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); + if (string.IsNullOrWhiteSpace(roleName)) throw new ArgumentException("Value cannot be null or whitespace.", "roleName"); - if (user.AllowedSections.InvariantContains(roleName) == false) return Task.FromResult(0); + var userRole = user.Roles.SingleOrDefault(r => r.RoleId == roleName); - var asInt = user.Id.TryConvertTo(); - if (asInt == false) + if (userRole != null) { - throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); - } - - var found = _userService.GetUserById(asInt.Result); - - if (found != null) - { - found.RemoveAllowedSection(roleName); + user.Roles.Remove(userRole); } return Task.FromResult(0); } /// - /// Returns the roles for this user + /// Returns the roles (user groups) for this user /// /// /// @@ -455,7 +447,7 @@ namespace Umbraco.Core.Security { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); - return Task.FromResult((IList)user.AllowedSections.ToList()); + return Task.FromResult((IList)user.Roles.Select(x => x.RoleId).ToList()); } /// @@ -467,7 +459,7 @@ namespace Umbraco.Core.Security { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); - return Task.FromResult(user.AllowedSections.InvariantContains(roleName)); + return Task.FromResult(user.Roles.Select(x => x.RoleId).InvariantContains(roleName)); } /// @@ -496,7 +488,7 @@ namespace Umbraco.Core.Security //the stamp cannot be null, so if it is currently null then we'll just return a hash of the password return Task.FromResult(user.SecurityStamp.IsNullOrWhiteSpace() - ? user.PasswordHash.ToMd5() + ? user.PasswordHash.GenerateHash() : user.SecurityStamp); } @@ -622,28 +614,40 @@ namespace Umbraco.Core.Security } #endregion - private bool UpdateMemberProperties(Models.Membership.IUser user, BackOfficeIdentityUser identityUser) + private bool UpdateMemberProperties(IUser user, BackOfficeIdentityUser identityUser) { var anythingChanged = false; - //don't assign anything if nothing has changed as this will trigger - //the track changes of the model - if ((user.LastLoginDate != default(DateTime) && identityUser.LastLoginDateUtc.HasValue == false) + + //don't assign anything if nothing has changed as this will trigger the track changes of the model + + if (identityUser.IsPropertyDirty("LastLoginDateUtc") + || (user.LastLoginDate != default(DateTime) && identityUser.LastLoginDateUtc.HasValue == false) || identityUser.LastLoginDateUtc.HasValue && user.LastLoginDate.ToUniversalTime() != identityUser.LastLoginDateUtc.Value) { anythingChanged = true; user.LastLoginDate = identityUser.LastLoginDateUtc.Value.ToLocalTime(); } - if (user.Name != identityUser.Name && identityUser.Name.IsNullOrWhiteSpace() == false) + if (identityUser.IsPropertyDirty("EmailConfirmed") + || (user.EmailConfirmedDate.HasValue && user.EmailConfirmedDate.Value != default(DateTime) && identityUser.EmailConfirmed == false) + || ((user.EmailConfirmedDate.HasValue == false || user.EmailConfirmedDate.Value == default(DateTime)) && identityUser.EmailConfirmed)) + { + anythingChanged = true; + user.EmailConfirmedDate = identityUser.EmailConfirmed ? (DateTime?)DateTime.Now : null; + } + if (identityUser.IsPropertyDirty("Name") + && user.Name != identityUser.Name && identityUser.Name.IsNullOrWhiteSpace() == false) { anythingChanged = true; user.Name = identityUser.Name; } - if (user.Email != identityUser.Email && identityUser.Email.IsNullOrWhiteSpace() == false) + if (identityUser.IsPropertyDirty("Email") + && user.Email != identityUser.Email && identityUser.Email.IsNullOrWhiteSpace() == false) { anythingChanged = true; user.Email = identityUser.Email; } - if (user.FailedPasswordAttempts != identityUser.AccessFailedCount) + if (identityUser.IsPropertyDirty("AccessFailedCount") + && user.FailedPasswordAttempts != identityUser.AccessFailedCount) { anythingChanged = true; user.FailedPasswordAttempts = identityUser.AccessFailedCount; @@ -660,31 +664,36 @@ namespace Umbraco.Core.Security } } - if (user.Username != identityUser.UserName && identityUser.UserName.IsNullOrWhiteSpace() == false) + if (identityUser.IsPropertyDirty("UserName") + && user.Username != identityUser.UserName && identityUser.UserName.IsNullOrWhiteSpace() == false) { anythingChanged = true; user.Username = identityUser.UserName; } - if (user.RawPasswordValue != identityUser.PasswordHash && identityUser.PasswordHash.IsNullOrWhiteSpace() == false) + if (identityUser.IsPropertyDirty("PasswordHash") + && user.RawPasswordValue != identityUser.PasswordHash && identityUser.PasswordHash.IsNullOrWhiteSpace() == false) { anythingChanged = true; user.RawPasswordValue = identityUser.PasswordHash; } - if (user.Language != identityUser.Culture && identityUser.Culture.IsNullOrWhiteSpace() == false) + if (identityUser.IsPropertyDirty("Culture") + && user.Language != identityUser.Culture && identityUser.Culture.IsNullOrWhiteSpace() == false) { anythingChanged = true; user.Language = identityUser.Culture; } - if (user.StartMediaId != identityUser.StartMediaId) + if (identityUser.IsPropertyDirty("StartMediaIds") + && user.StartMediaIds.UnsortedSequenceEqual(identityUser.StartMediaIds) == false) { anythingChanged = true; - user.StartMediaId = identityUser.StartMediaId; + user.StartMediaIds = identityUser.StartMediaIds; } - if (user.StartContentId != identityUser.StartContentId) + if (identityUser.IsPropertyDirty("StartContentIds") + && user.StartContentIds.UnsortedSequenceEqual(identityUser.StartContentIds) == false) { anythingChanged = true; - user.StartContentId = identityUser.StartContentId; + user.StartContentIds = identityUser.StartContentIds; } if (user.SecurityStamp != identityUser.SecurityStamp) { @@ -692,19 +701,44 @@ namespace Umbraco.Core.Security user.SecurityStamp = identityUser.SecurityStamp; } - if (user.AllowedSections.ContainsAll(identityUser.AllowedSections) == false - || identityUser.AllowedSections.ContainsAll(user.AllowedSections) == false) + //TODO: Fix this for Groups too + if (identityUser.IsPropertyDirty("Roles") || identityUser.IsPropertyDirty("Groups")) { - anythingChanged = true; - foreach (var allowedSection in user.AllowedSections) + var userGroupAliases = user.Groups.Select(x => x.Alias).ToArray(); + + var identityUserRoles = identityUser.Roles.Select(x => x.RoleId).ToArray(); + var identityUserGroups = identityUser.Groups.Select(x => x.Alias).ToArray(); + + var combinedAliases = identityUserRoles.Union(identityUserGroups).ToArray(); + + if (userGroupAliases.ContainsAll(combinedAliases) == false + || combinedAliases.ContainsAll(userGroupAliases) == false) { - user.RemoveAllowedSection(allowedSection); - } - foreach (var allowedApplication in identityUser.AllowedSections) - { - user.AddAllowedSection(allowedApplication); + anythingChanged = true; + + //clear out the current groups (need to ToArray since we are modifying the iterator) + user.ClearGroups(); + + //go lookup all these groups + var groups = _userService.GetUserGroupsByAlias(combinedAliases).Select(x => x.ToReadOnlyGroup()).ToArray(); + + //use all of the ones assigned and add them + foreach (var group in groups) + { + user.AddGroup(group); + } + + //re-assign + identityUser.Groups = groups; } } + + //we should re-set the calculated start nodes + identityUser.CalculatedMediaStartNodeIds = user.CalculateMediaStartNodeIds(_entityService); + identityUser.CalculatedContentStartNodeIds = user.CalculateContentStartNodeIds(_entityService); + + //reset all changes + identityUser.ResetDirtyProperties(false); return anythingChanged; } @@ -715,7 +749,5 @@ namespace Umbraco.Core.Security if (_disposed) throw new ObjectDisposedException(GetType().Name); } - - } } diff --git a/src/Umbraco.Core/Security/BackOfficeUserValidator.cs b/src/Umbraco.Core/Security/BackOfficeUserValidator.cs new file mode 100644 index 0000000000..58319e95a7 --- /dev/null +++ b/src/Umbraco.Core/Security/BackOfficeUserValidator.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using Microsoft.AspNet.Identity; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Models.Identity; + +namespace Umbraco.Core.Security +{ + /// + /// Custom validator to not validate a user's username or email if they haven't changed + /// + /// + internal class BackOfficeUserValidator : UserValidator + where T : BackOfficeIdentityUser + { + public BackOfficeUserValidator(UserManager manager) : base(manager) + { + } + + public override async Task ValidateAsync(T item) + { + //Don't validate if the user's email or username hasn't changed otherwise it's just wasting SQL queries. + if (item.IsPropertyDirty("Email") || item.IsPropertyDirty("UserName")) + { + return await base.ValidateAsync(item); + } + return IdentityResult.Success; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/EmailService.cs b/src/Umbraco.Core/Security/EmailService.cs index c32888bcb4..f8f9af10ae 100644 --- a/src/Umbraco.Core/Security/EmailService.cs +++ b/src/Umbraco.Core/Security/EmailService.cs @@ -1,6 +1,7 @@ using System.Net.Mail; using System.Threading.Tasks; using Microsoft.AspNet.Identity; +using Umbraco.Core.Configuration; namespace Umbraco.Core.Security { @@ -8,18 +9,33 @@ namespace Umbraco.Core.Security { public async Task SendAsync(IdentityMessage message) { - using (var client = new SmtpClient()) - using (var mailMessage = new MailMessage()) + var mailMessage = new MailMessage( + UmbracoConfig.For.UmbracoSettings().Content.NotificationEmailAddress, + message.Destination, + message.Subject, + message.Body) { - mailMessage.Body = message.Body; - mailMessage.To.Add(message.Destination); - mailMessage.Subject = message.Subject; + IsBodyHtml = message.Body.IsNullOrWhiteSpace() == false + && message.Body.Contains("<") && message.Body.Contains(" public interface IBackOfficeUserPasswordChecker { + /// + /// Checks a password for a user + /// + /// + /// + /// + /// + /// This will allow a developer to auto-link a local account which is required if the user queried doesn't exist locally. + /// The user parameter will always contain the username, if the user doesn't exist locally, the other properties will not be filled in. + /// A developer can then create a local account by filling in the properties and using UserManager.CreateAsync + /// Task CheckPasswordAsync(BackOfficeIdentityUser user, string password); } -} +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/IMembershipProviderPasswordHasher.cs b/src/Umbraco.Core/Security/IMembershipProviderPasswordHasher.cs new file mode 100644 index 0000000000..42715d280a --- /dev/null +++ b/src/Umbraco.Core/Security/IMembershipProviderPasswordHasher.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNet.Identity; + +namespace Umbraco.Core.Security +{ + /// + /// A password hasher that is based on the rules configured for a membership provider + /// + public interface IMembershipProviderPasswordHasher : IPasswordHasher + { + MembershipProviderBase MembershipProvider { get; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/IUserAwarePasswordHasher.cs b/src/Umbraco.Core/Security/IUserAwarePasswordHasher.cs new file mode 100644 index 0000000000..cea4d5a144 --- /dev/null +++ b/src/Umbraco.Core/Security/IUserAwarePasswordHasher.cs @@ -0,0 +1,18 @@ +using System; +using Microsoft.AspNet.Identity; + +namespace Umbraco.Core.Security +{ + /// + /// A password hasher that is User aware so that it can process the hashing based on the user's settings + /// + /// + /// + public interface IUserAwarePasswordHasher + where TUser : class, IUser + where TKey : IEquatable + { + string HashPassword(TUser user, string password); + PasswordVerificationResult VerifyHashedPassword(TUser user, string hashedPassword, string providedPassword); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/MachineKeyGenerator.cs b/src/Umbraco.Core/Security/MachineKeyGenerator.cs new file mode 100644 index 0000000000..9dd06f44bd --- /dev/null +++ b/src/Umbraco.Core/Security/MachineKeyGenerator.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Core.Security +{ + /// + /// Used to generate a machine key + /// + internal class MachineKeyGenerator + { + /// + /// Generates the string to be stored in the web.config + /// + /// + /// + /// Machine key details are here: https://msdn.microsoft.com/en-us/library/vstudio/w8h3skw9%28v=vs.100%29.aspx?f=255&MSPPError=-2147217396 + /// + public string GenerateConfigurationBlock() + { + var c = @""; + + var Xxx = 3; + + return string.Format(c, GenerateAESDecryptionKey(), GenerateHMACSHA256ValidationKey()); + } + + public string GenerateHMACSHA256ValidationKey() + { + //See: https://msdn.microsoft.com/en-us/library/vstudio/w8h3skw9%28v=vs.100%29.aspx?f=255&MSPPError=-2147217396 + //See: https://msdn.microsoft.com/en-us/library/ff649308.aspx?f=255&MSPPError=-2147217396 + /* + key value Specifies a manually assigned key. + The validationKey value must be manually set to a string of hexadecimal + characters to ensure consistent configuration across all servers in a Web farm. + The length of the key depends on the hash algorithm that is used: + + AES requires a 256-bit key (64 hexadecimal characters). + MD5 requires a 128-bit key (32 hexadecimal characters). + SHA1 requires a 160-bit key (40 hexadecimal characters). + 3DES requires a 192-bit key (48 hexadecimal characters). + HMACSHA256 requires a 256-bit key (64 hexadecimal characters) == DEFAULT + HMACSHA384 requires a 384-bit key (96 hexadecimal characters). + HMACSHA512 requires a 512-bit key (128 hexadecimal characters). + */ + + //64 in length = 256 bits + return GenerateKey(64); + } + + public string GenerateAESDecryptionKey() + { + //See: //See: https://msdn.microsoft.com/en-us/library/vstudio/w8h3skw9%28v=vs.100%29.aspx?f=255&MSPPError=-2147217396 + /* + key value Specifies a manually assigned key. + The decryptionKey value must be manually set to a string of + hexadecimal characters to ensure consistent configuration across all servers in a Web farm. + The key should be 64 bits (16 hexadecimal characters) long for DES encryption, or 192 bits + (48 hexadecimal characters) long for 3DES. For AES, the key can be 128 bits (32 characters), + 192 bits (48 characters), or 256 bits (64 characters) long. + */ + + //64 in length = 256 bits + return GenerateKey(64); + } + + private string GenerateKey(int len = 64) + { + var buff = new byte[len / 2]; + var rng = new RNGCryptoServiceProvider(); + rng.GetBytes(buff); + var sb = new StringBuilder(len); + + for (int i = 0; i < buff.Length; i++) + sb.Append(string.Format("{0:X2}", buff[i])); + + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/MembershipPasswordHasher.cs b/src/Umbraco.Core/Security/MembershipPasswordHasher.cs deleted file mode 100644 index 14e7c69322..0000000000 --- a/src/Umbraco.Core/Security/MembershipPasswordHasher.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.AspNet.Identity; - -namespace Umbraco.Core.Security -{ - /// - /// A custom password hasher that conforms to the current password hashing done in Umbraco - /// - internal class MembershipPasswordHasher : IPasswordHasher - { - private readonly MembershipProviderBase _provider; - - public MembershipPasswordHasher(MembershipProviderBase provider) - { - _provider = provider; - } - - public string HashPassword(string password) - { - return _provider.HashPasswordForStorage(password); - } - - public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword) - { - return _provider.VerifyPassword(providedPassword, hashedPassword) - ? PasswordVerificationResult.Success - : PasswordVerificationResult.Failed; - } - - - } -} diff --git a/src/Umbraco.Core/Security/MembershipProviderBase.cs b/src/Umbraco.Core/Security/MembershipProviderBase.cs index eff71b0a9d..013e80b020 100644 --- a/src/Umbraco.Core/Security/MembershipProviderBase.cs +++ b/src/Umbraco.Core/Security/MembershipProviderBase.cs @@ -29,23 +29,24 @@ namespace Umbraco.Core.Security public bool VerifyPassword(string password, string hashedPassword) { + if (string.IsNullOrWhiteSpace(hashedPassword)) throw new ArgumentException("Value cannot be null or whitespace.", "hashedPassword"); return CheckPassword(password, hashedPassword); } /// - /// Providers can override this setting, default is 7 + /// Providers can override this setting, default is 10 /// public virtual int DefaultMinPasswordLength { - get { return 7; } + get { return 10; } } /// - /// Providers can override this setting, default is 1 + /// Providers can override this setting, default is 0 /// public virtual int DefaultMinNonAlphanumericChars { - get { return 1; } + get { return 0; } } /// @@ -225,7 +226,7 @@ namespace Umbraco.Core.Security base.Initialize(name, config); _enablePasswordRetrieval = config.GetValue("enablePasswordRetrieval", false); - _enablePasswordReset = config.GetValue("enablePasswordReset", false); + _enablePasswordReset = config.GetValue("enablePasswordReset", true); _requiresQuestionAndAnswer = config.GetValue("requiresQuestionAndAnswer", false); _requiresUniqueEmail = config.GetValue("requiresUniqueEmail", true); _maxInvalidPasswordAttempts = GetIntValue(config, "maxInvalidPasswordAttempts", 5, false, 0); @@ -674,7 +675,7 @@ namespace Umbraco.Core.Security if (PasswordFormat == MembershipPasswordFormat.Clear) return pass; var bytes = Encoding.Unicode.GetBytes(pass); - var numArray1 = Convert.FromBase64String(salt); + var saltBytes = Convert.FromBase64String(salt); byte[] inArray; if (PasswordFormat == MembershipPasswordFormat.Hashed) @@ -684,22 +685,27 @@ namespace Umbraco.Core.Security if (algorithm != null) { var keyedHashAlgorithm = algorithm; - if (keyedHashAlgorithm.Key.Length == numArray1.Length) - keyedHashAlgorithm.Key = numArray1; - else if (keyedHashAlgorithm.Key.Length < numArray1.Length) - { + if (keyedHashAlgorithm.Key.Length == saltBytes.Length) + { + //if the salt bytes is the required key length for the algorithm, use it as-is + keyedHashAlgorithm.Key = saltBytes; + } + else if (keyedHashAlgorithm.Key.Length < saltBytes.Length) + { + //if the salt bytes is too long for the required key length for the algorithm, reduce it var numArray2 = new byte[keyedHashAlgorithm.Key.Length]; - Buffer.BlockCopy(numArray1, 0, numArray2, 0, numArray2.Length); + Buffer.BlockCopy(saltBytes, 0, numArray2, 0, numArray2.Length); keyedHashAlgorithm.Key = numArray2; } else { + //if the salt bytes is too long for the required key length for the algorithm, extend it var numArray2 = new byte[keyedHashAlgorithm.Key.Length]; var dstOffset = 0; while (dstOffset < numArray2.Length) { - var count = Math.Min(numArray1.Length, numArray2.Length - dstOffset); - Buffer.BlockCopy(numArray1, 0, numArray2, dstOffset, count); + var count = Math.Min(saltBytes.Length, numArray2.Length - dstOffset); + Buffer.BlockCopy(saltBytes, 0, numArray2, dstOffset, count); dstOffset += count; } keyedHashAlgorithm.Key = numArray2; @@ -708,9 +714,9 @@ namespace Umbraco.Core.Security } else { - var buffer = new byte[numArray1.Length + bytes.Length]; - Buffer.BlockCopy(numArray1, 0, buffer, 0, numArray1.Length); - Buffer.BlockCopy(bytes, 0, buffer, numArray1.Length, bytes.Length); + var buffer = new byte[saltBytes.Length + bytes.Length]; + Buffer.BlockCopy(saltBytes, 0, buffer, 0, saltBytes.Length); + Buffer.BlockCopy(bytes, 0, buffer, saltBytes.Length, bytes.Length); inArray = hashAlgorithm.ComputeHash(buffer); } } @@ -718,9 +724,9 @@ namespace Umbraco.Core.Security { //this code is copied from the sql membership provider - pretty sure this could be nicely re-written to completely // ignore the salt stuff since we are not salting the password when encrypting. - var password = new byte[numArray1.Length + bytes.Length]; - Buffer.BlockCopy(numArray1, 0, password, 0, numArray1.Length); - Buffer.BlockCopy(bytes, 0, password, numArray1.Length, bytes.Length); + var password = new byte[saltBytes.Length + bytes.Length]; + Buffer.BlockCopy(saltBytes, 0, password, 0, saltBytes.Length); + Buffer.BlockCopy(bytes, 0, password, saltBytes.Length, bytes.Length); inArray = EncryptPassword(password, MembershipPasswordCompatibilityMode.Framework40); } return Convert.ToBase64String(inArray); @@ -734,6 +740,7 @@ namespace Umbraco.Core.Security /// protected internal bool CheckPassword(string password, string dbPassword) { + if (string.IsNullOrWhiteSpace(dbPassword)) throw new ArgumentException("Value cannot be null or whitespace.", "dbPassword"); switch (PasswordFormat) { case MembershipPasswordFormat.Encrypted: @@ -795,6 +802,7 @@ namespace Umbraco.Core.Security /// internal string StoredPassword(string storedString, out string salt) { + if (string.IsNullOrWhiteSpace(storedString)) throw new ArgumentException("Value cannot be null or whitespace.", "storedString"); if (UseLegacyEncoding) { salt = string.Empty; diff --git a/src/Umbraco.Core/Security/MembershipProviderExtensions.cs b/src/Umbraco.Core/Security/MembershipProviderExtensions.cs index cd23388a39..71581ebeb8 100644 --- a/src/Umbraco.Core/Security/MembershipProviderExtensions.cs +++ b/src/Umbraco.Core/Security/MembershipProviderExtensions.cs @@ -1,16 +1,11 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Security.Principal; -using System.Text; using System.Threading; -using System.Threading.Tasks; using System.Web; using System.Web.Hosting; using System.Web.Security; using Umbraco.Core.Configuration; using Umbraco.Core.Models; -using Umbraco.Core.Security; using Umbraco.Core.Services; namespace Umbraco.Core.Security @@ -31,13 +26,14 @@ namespace Umbraco.Core.Security if (userService == null) return canReset; - //we need to check for the special case in which a user is an admin - in which acse they can reset the password even if EnablePasswordReset == false + //we need to check for the special case in which a user is an admin - in which case they can reset the password even if EnablePasswordReset == false if (provider.EnablePasswordReset == false) { var identity = Thread.CurrentPrincipal.GetUmbracoIdentity(); if (identity != null) { - var user = userService.GetByUsername(identity.Username); + var user = userService.GetUserById(identity.Id.TryConvertTo().Result); + if (user == null) throw new InvalidOperationException("No user with username " + identity.Username + " found"); var userIsAdmin = user.IsAdmin(); if (userIsAdmin) { @@ -70,7 +66,7 @@ namespace Umbraco.Core.Security } /// - /// Method to get the Umbraco Members membership provider based on it's alias + /// Method to get the Umbraco Members membership provider based on its alias /// /// public static MembershipProvider GetMembersMembershipProvider() @@ -83,7 +79,7 @@ namespace Umbraco.Core.Security } /// - /// Method to get the Umbraco Users membership provider based on it's alias + /// Method to get the Umbraco Users membership provider based on its alias /// /// public static MembershipProvider GetUsersMembershipProvider() @@ -167,6 +163,5 @@ namespace Umbraco.Core.Security { return (UmbracoMembershipProviderBase)membershipProvider; } - } } diff --git a/src/Umbraco.Core/Security/MembershipProviderPasswordHasher.cs b/src/Umbraco.Core/Security/MembershipProviderPasswordHasher.cs new file mode 100644 index 0000000000..f518f99c55 --- /dev/null +++ b/src/Umbraco.Core/Security/MembershipProviderPasswordHasher.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNet.Identity; + +namespace Umbraco.Core.Security +{ + /// + /// A password hasher that conforms to the password hashing done with membership providers + /// + public class MembershipProviderPasswordHasher : IMembershipProviderPasswordHasher + { + /// + /// Exposes the underlying MembershipProvider + /// + public MembershipProviderBase MembershipProvider { get; private set; } + + public MembershipProviderPasswordHasher(MembershipProviderBase provider) + { + MembershipProvider = provider; + } + + public string HashPassword(string password) + { + return MembershipProvider.HashPasswordForStorage(password); + } + + public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword) + { + return MembershipProvider.VerifyPassword(providedPassword, hashedPassword) + ? PasswordVerificationResult.Success + : PasswordVerificationResult.Failed; + } + + + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/MembershipProviderPasswordValidator.cs b/src/Umbraco.Core/Security/MembershipProviderPasswordValidator.cs new file mode 100644 index 0000000000..3331116b4e --- /dev/null +++ b/src/Umbraco.Core/Security/MembershipProviderPasswordValidator.cs @@ -0,0 +1,38 @@ +using System.Threading.Tasks; +using System.Web.Security; +using Microsoft.AspNet.Identity; + +namespace Umbraco.Core.Security +{ + /// + /// Ensure that both the normal password validator rules are processed along with the underlying memberhsip provider rules + /// + public class MembershipProviderPasswordValidator : PasswordValidator + { + public MembershipProvider Provider { get; private set; } + + public MembershipProviderPasswordValidator(MembershipProvider provider) + { + Provider = provider; + + RequiredLength = Provider.MinRequiredPasswordLength; + RequireNonLetterOrDigit = Provider.MinRequiredNonAlphanumericCharacters > 0; + RequireDigit = false; + RequireLowercase = false; + RequireUppercase = false; + } + + public override async Task ValidateAsync(string item) + { + var result = await base.ValidateAsync(item); + if (result.Succeeded == false) + return result; + var providerValidate = MembershipProviderBase.IsPasswordValid(item, Provider.MinRequiredNonAlphanumericCharacters, Provider.PasswordStrengthRegularExpression, Provider.MinRequiredPasswordLength); + if (providerValidate.Success == false) + { + return IdentityResult.Failed("Could not set password, password rules violated: " + providerValidate.Result); + } + return IdentityResult.Success; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs index cf6a77205a..1341a2ed64 100644 --- a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs +++ b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs @@ -48,12 +48,20 @@ namespace Umbraco.Core.Security || realName == null || session == null) throw new InvalidOperationException("Cannot create a " + typeof(UmbracoBackOfficeIdentity) + " from " + typeof(ClaimsIdentity) + " since there are missing required claims"); - int startContentIdAsInt; - int startMediaIdAsInt; - if (int.TryParse(startContentId, out startContentIdAsInt) == false || int.TryParse(startMediaId, out startMediaIdAsInt) == false) + int[] startContentIdsAsInt; + int[] startMediaIdsAsInt; + if (startContentId.DetectIsJson() == false || startMediaId.DetectIsJson() == false) + throw new InvalidOperationException("Cannot create a " + typeof(UmbracoBackOfficeIdentity) + " from " + typeof(ClaimsIdentity) + " since the data is not formatted correctly - either content or media start Ids are not JSON"); + + try { - throw new InvalidOperationException("Cannot create a " + typeof(UmbracoBackOfficeIdentity) + " from " + typeof(ClaimsIdentity) + " since the data is not formatted correctly"); + startContentIdsAsInt = JsonConvert.DeserializeObject(startContentId); + startMediaIdsAsInt = JsonConvert.DeserializeObject(startMediaId); } + catch (Exception e) + { + throw new InvalidOperationException("Cannot create a " + typeof(UmbracoBackOfficeIdentity) + " from " + typeof(ClaimsIdentity) + " since the data is not formatted correctly - either content or media start Ids could not be parsed as JSON", e); + } var roles = identity.FindAll(x => x.Type == DefaultRoleClaimType).Select(role => role.Value).ToList(); var allowedApps = identity.FindAll(x => x.Type == Constants.Security.AllowedApplicationsClaimType).Select(app => app.Value).ToList(); @@ -67,8 +75,8 @@ namespace Umbraco.Core.Security Roles = roles.ToArray(), Username = username, RealName = realName, - StartContentNode = startContentIdAsInt, - StartMediaNode = startMediaIdAsInt + StartContentNodes = startContentIdsAsInt, + StartMediaNodes = startMediaIdsAsInt }; return new UmbracoBackOfficeIdentity(identity, userData); @@ -202,10 +210,10 @@ namespace Umbraco.Core.Security AddClaim(new Claim(ClaimTypes.GivenName, UserData.RealName, ClaimValueTypes.String, Issuer, Issuer, this)); if (HasClaim(x => x.Type == Constants.Security.StartContentNodeIdClaimType) == false) - AddClaim(new Claim(Constants.Security.StartContentNodeIdClaimType, StartContentNode.ToInvariantString(), ClaimValueTypes.Integer32, Issuer, Issuer, this)); + AddClaim(new Claim(Constants.Security.StartContentNodeIdClaimType, JsonConvert.SerializeObject(StartContentNodes), ClaimValueTypes.Integer32, Issuer, Issuer, this)); if (HasClaim(x => x.Type == Constants.Security.StartMediaNodeIdClaimType) == false) - AddClaim(new Claim(Constants.Security.StartMediaNodeIdClaimType, StartMediaNode.ToInvariantString(), ClaimValueTypes.Integer32, Issuer, Issuer, this)); + AddClaim(new Claim(Constants.Security.StartMediaNodeIdClaimType, JsonConvert.SerializeObject(StartMediaNodes), ClaimValueTypes.Integer32, Issuer, Issuer, this)); if (HasClaim(x => x.Type == ClaimTypes.Locality) == false) AddClaim(new Claim(ClaimTypes.Locality, Culture, ClaimValueTypes.String, Issuer, Issuer, this)); @@ -259,14 +267,14 @@ namespace Umbraco.Core.Security get { return _currentIssuer; } } - public int StartContentNode + public int[] StartContentNodes { - get { return UserData.StartContentNode; } + get { return UserData.StartContentNodes; } } - public int StartMediaNode + public int[] StartMediaNodes { - get { return UserData.StartMediaNode; } + get { return UserData.StartMediaNodes; } } public string[] AllowedApplications diff --git a/src/Umbraco.Core/Security/UserAwareMembershipProviderPasswordHasher.cs b/src/Umbraco.Core/Security/UserAwareMembershipProviderPasswordHasher.cs new file mode 100644 index 0000000000..7d19d72c3c --- /dev/null +++ b/src/Umbraco.Core/Security/UserAwareMembershipProviderPasswordHasher.cs @@ -0,0 +1,30 @@ +using System; +using Microsoft.AspNet.Identity; +using Umbraco.Core.Models.Identity; + +namespace Umbraco.Core.Security +{ + /// + /// The default password hasher that is User aware so that it can process the hashing based on the user's settings + /// + public class UserAwareMembershipProviderPasswordHasher : MembershipProviderPasswordHasher, IUserAwarePasswordHasher + { + public UserAwareMembershipProviderPasswordHasher(MembershipProviderBase provider) : base(provider) + { + } + + public string HashPassword(BackOfficeIdentityUser user, string password) + { + //TODO: Implement the logic for this, we need to lookup the password format for the user and hash accordingly: http://issues.umbraco.org/issue/U4-10089 + //NOTE: For now this just falls back to the hashing we are currently using + return base.HashPassword(password); + } + + public PasswordVerificationResult VerifyHashedPassword(BackOfficeIdentityUser user, string hashedPassword, string providedPassword) + { + //TODO: Implement the logic for this, we need to lookup the password format for the user and hash accordingly: http://issues.umbraco.org/issue/U4-10089 + //NOTE: For now this just falls back to the hashing we are currently using + return base.VerifyHashedPassword(hashedPassword, providedPassword); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/UserData.cs b/src/Umbraco.Core/Security/UserData.cs index 80741caab8..c67c39f811 100644 --- a/src/Umbraco.Core/Security/UserData.cs +++ b/src/Umbraco.Core/Security/UserData.cs @@ -46,12 +46,18 @@ namespace Umbraco.Core.Security [DataMember(Name = "name")] public string RealName { get; set; } - + + /// + /// The start nodes on the UserData object for the auth ticket contains all of the user's start nodes including ones assigned to their user groups + /// [DataMember(Name = "startContent")] - public int StartContentNode { get; set; } - + public int[] StartContentNodes { get; set; } + + /// + /// The start nodes on the UserData object for the auth ticket contains all of the user's start nodes including ones assigned to their user groups + /// [DataMember(Name = "startMedia")] - public int StartMediaNode { get; set; } + public int[] StartMediaNodes { get; set; } [DataMember(Name = "allowedApps")] public string[] AllowedApplications { get; set; } diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 5d71fee38c..680bbf4e20 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -18,7 +18,7 @@ namespace Umbraco.Core.Services { //TODO: Remove this class in v8 - //TODO: There's probably more that needs to be added like the EmptyRecycleBin, etc... + //TODO: There's probably more that needs to be added like the EmptyRecycleBin, etc... /// /// Saves a single object @@ -100,6 +100,13 @@ namespace Umbraco.Core.Services /// public interface IContentService : IContentServiceBase { + IEnumerable GetBlueprintsForContentTypes(params int[] documentTypeIds); + IContent GetBlueprintById(int id); + IContent GetBlueprintById(Guid id); + void SaveBlueprint(IContent content, int userId = 0); + void DeleteBlueprint(IContent content, int userId = 0); + IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = 0); + int CountPublished(string contentTypeAlias = null); int Count(string contentTypeAlias = null); int CountChildren(int parentId, string contentTypeAlias = null); @@ -107,25 +114,25 @@ namespace Umbraco.Core.Services /// /// Used to bulk update the permissions set for a content item. This will replace all permissions - /// assigned to an entity with a list of user id & permission pairs. + /// assigned to an entity with a list of user group id & permission pairs. /// /// void ReplaceContentPermissions(EntityPermissionSet permissionSet); /// - /// Assigns a single permission to the current content item for the specified user ids + /// Assigns a single permission to the current content item for the specified user group ids /// /// /// - /// - void AssignContentPermission(IContent entity, char permission, IEnumerable userIds); + /// + void AssignContentPermission(IContent entity, char permission, IEnumerable groupIds); /// - /// Gets the list of permissions for the content item + /// Returns implicit/inherited permissions assigned to the content item for all user groups /// /// /// - IEnumerable GetPermissionsForEntity(IContent content); + EntityPermissionCollection GetPermissionsForEntity(IContent content); bool SendToPublication(IContent content, int userId = 0); diff --git a/src/Umbraco.Core/Services/IContentTypeService.cs b/src/Umbraco.Core/Services/IContentTypeService.cs index 51b78ee060..e7a2b60608 100644 --- a/src/Umbraco.Core/Services/IContentTypeService.cs +++ b/src/Umbraco.Core/Services/IContentTypeService.cs @@ -24,5 +24,12 @@ namespace Umbraco.Core.Services /// /// IEnumerable GetAllContentTypeAliases(params Guid[] objectTypes); + + /// + /// Returns all content type Ids for the aliases given + /// + /// + /// + IEnumerable GetAllContentTypeIds(string[] aliases); } } diff --git a/src/Umbraco.Core/Services/IEntityService.cs b/src/Umbraco.Core/Services/IEntityService.cs index e0070bdc68..9c16af5ed4 100644 --- a/src/Umbraco.Core/Services/IEntityService.cs +++ b/src/Umbraco.Core/Services/IEntityService.cs @@ -28,7 +28,14 @@ namespace Umbraco.Core.Services /// /// /// - Attempt GetIdForKey(Guid key, UmbracoObjectTypes umbracoObjectType); + Attempt GetIdForKey(Guid key, UmbracoObjectTypes umbracoObjectType); + + /// + /// Returns the integer id for a given Udi + /// + /// + /// + Attempt GetIdForUdi(Udi udi); /// /// Returns the GUID for a given integer id @@ -168,6 +175,12 @@ namespace Umbraco.Core.Services IEnumerable GetPagedDescendants(int id, UmbracoObjectTypes umbracoObjectType, long pageIndex, int pageSize, out long totalRecords, string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = ""); + /// + /// Returns a paged collection of descendants + /// + IEnumerable GetPagedDescendants(IEnumerable ids, UmbracoObjectTypes umbracoObjectType, long pageIndex, int pageSize, out long totalRecords, + string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = ""); + /// /// Returns a paged collection of descendants from the root /// @@ -236,6 +249,16 @@ namespace Umbraco.Core.Services /// An enumerable list of objects IEnumerable GetAll(Guid objectTypeId, params int[] ids); + /// + /// Gets paths for entities. + /// + IEnumerable GetAllPaths(UmbracoObjectTypes umbracoObjectType, params int[] ids); + + /// + /// Gets paths for entities. + /// + IEnumerable GetAllPaths(UmbracoObjectTypes umbracoObjectType, params Guid[] keys); + /// /// Gets the UmbracoObjectType from the integer id of an IUmbracoEntity. /// diff --git a/src/Umbraco.Core/Services/IMembershipMemberService.cs b/src/Umbraco.Core/Services/IMembershipMemberService.cs index 0be76a7fb7..e6f3d62421 100644 --- a/src/Umbraco.Core/Services/IMembershipMemberService.cs +++ b/src/Umbraco.Core/Services/IMembershipMemberService.cs @@ -64,6 +64,18 @@ namespace Umbraco.Core.Services /// T CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias); + /// + /// Creates and persists a new + /// + /// An can be of type or + /// Username of the to create + /// Email of the to create + /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database + /// Alias of the Type + /// IsApproved of the to create + /// + T CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias, bool isApproved); + /// /// Gets an by its provider key /// @@ -113,6 +125,14 @@ namespace Umbraco.Core.Services /// Default is True otherwise set to False to not raise events void Save(IEnumerable entities, bool raiseEvents = true); + /// + /// Gets the default MemberType alias + /// + /// By default we'll return the 'writer', but we need to check it exists. If it doesn't we'll + /// return the first type that is not an admin, otherwise if there's only one we will return that one. + /// Alias of the default MemberType + string GetDefaultMemberType(); + /// /// Finds a list of objects by a partial email string /// diff --git a/src/Umbraco.Core/Services/IMembershipUserService.cs b/src/Umbraco.Core/Services/IMembershipUserService.cs index 6379f966e5..f770ec0359 100644 --- a/src/Umbraco.Core/Services/IMembershipUserService.cs +++ b/src/Umbraco.Core/Services/IMembershipUserService.cs @@ -1,4 +1,5 @@ -using Umbraco.Core.Models.Membership; +using System.Collections.Generic; +using Umbraco.Core.Models.Membership; namespace Umbraco.Core.Services { @@ -18,8 +19,7 @@ namespace Umbraco.Core.Services /// Id of the user once its been created. /// Username of the User to create /// Email of the User to create - /// which the User should be based on /// - IUser CreateUserWithIdentity(string username, string email, IUserType userType); + IUser CreateUserWithIdentity(string username, string email); } } diff --git a/src/Umbraco.Core/Services/ISectionService.cs b/src/Umbraco.Core/Services/ISectionService.cs index 4f6dbaaa8c..132f66108f 100644 --- a/src/Umbraco.Core/Services/ISectionService.cs +++ b/src/Umbraco.Core/Services/ISectionService.cs @@ -11,7 +11,7 @@ namespace Umbraco.Core.Services IEnumerable
GetSections(); /// - /// Get the user's allowed sections + /// Get the user group's allowed sections /// /// /// diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs index 1b7f3985d5..3fe2886cbd 100644 --- a/src/Umbraco.Core/Services/IUserService.cs +++ b/src/Umbraco.Core/Services/IUserService.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Core.Models.Membership; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Core.Services { @@ -9,6 +10,29 @@ namespace Umbraco.Core.Services ///
public interface IUserService : IMembershipUserService { + /// + /// This is basically facets of UserStates key = state, value = count + /// + IDictionary GetUserStates(); + + /// + /// Get paged users + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, + UserState[] userState = null, + string[] userGroups = null, + string filter = ""); + /// /// This is simply a helper method which essentially just wraps the MembershipProvider's ChangePassword method /// @@ -45,95 +69,143 @@ namespace Umbraco.Core.Services ///
/// Id of the user to retrieve /// - IUser GetUserById(int id); + IUser GetUserById(int id); /// - /// Removes a specific section from all users + /// Gets a users by Id + /// + /// Ids of the users to retrieve + /// + IEnumerable GetUsersById(params int[] ids); + + /// + /// Removes a specific section from all user groups /// /// This is useful when an entire section is removed from config /// Alias of the section to remove - void DeleteSectionFromAllUsers(string sectionAlias); + void DeleteSectionFromAllUserGroups(string sectionAlias); /// - /// Add a specific section to all users or those specified as parameters - /// - /// This is useful when a new section is created to allow specific users accessing it - /// Alias of the section to add - /// Specifiying nothing will add the section to all user - void AddSectionToAllUsers(string sectionAlias, params int[] userIds); - - /// - /// Get permissions set for a user and optional node ids + /// Get explicitly assigned permissions for a user and optional node ids /// /// If no permissions are found for a particular entity then the user's default permissions will be applied /// User to retrieve permissions for - /// Specifiying nothing will return all user permissions for all nodes + /// Specifiying nothing will return all user permissions for all nodes that have explicit permissions defined /// An enumerable list of - IEnumerable GetPermissions(IUser user, params int[] nodeIds); + /// + /// This will return the default permissions for the user's groups for node ids that don't have explicitly defined permissions + /// + EntityPermissionCollection GetPermissions(IUser user, params int[] nodeIds); /// - /// Replaces the same permission set for a single user to any number of entities + /// Get explicitly assigned permissions for groups and optional node Ids /// - /// Id of the user + /// + /// + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set + /// + /// Specifiying nothing will return all permissions for all nodes + /// An enumerable list of + EntityPermissionCollection GetPermissions(IUserGroup[] groups, bool fallbackToDefaultPermissions, params int[] nodeIds); + + /// + /// Gets the implicit/inherited permissions for the user for the given path + /// + /// User to check permissions for + /// Path to check permissions for + EntityPermissionSet GetPermissionsForPath(IUser user, string path); + + /// + /// Gets the permissions for the provided groups and path + /// + /// + /// Path to check permissions for + /// + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set + /// + EntityPermissionSet GetPermissionsForPath(IUserGroup[] groups, string path, bool fallbackToDefaultPermissions = false); + + /// + /// Replaces the same permission set for a single group to any number of entities + /// + /// Id of the group /// - /// Permissions as enumerable list of , - /// if no permissions are specified then all permissions for this node are removed for this user + /// Permissions as enumerable list of , + /// if no permissions are specified then all permissions for this node are removed for this group /// /// Specify the nodes to replace permissions for. If nothing is specified all permissions are removed. - /// If no 'entityIds' are specified all permissions will be removed for the specified user. - void ReplaceUserPermissions(int userId, IEnumerable permissions, params int[] entityIds); + /// If no 'entityIds' are specified all permissions will be removed for the specified group. + void ReplaceUserGroupPermissions(int groupId, IEnumerable permissions, params int[] entityIds); /// - /// Assigns the same permission set for a single user to any number of entities + /// Assigns the same permission set for a single user group to any number of entities /// - /// Id of the user + /// Id of the group /// /// Specify the nodes to replace permissions for - void AssignUserPermission(int userId, char permission, params int[] entityIds); - - #region User types + void AssignUserGroupPermission(int groupId, char permission, params int[] entityIds); /// - /// Gets all UserTypes or thosed specified as parameters + /// Gets a list of objects associated with a given group /// - /// Optional Ids of UserTypes to retrieve - /// An enumerable list of - IEnumerable GetAllUserTypes(params int[] ids); + /// Id of group + /// + IEnumerable GetAllInGroup(int groupId); /// - /// Gets a UserType by its Alias + /// Gets a list of objects not associated with a given group /// - /// Alias of the UserType to retrieve - /// - IUserType GetUserTypeByAlias(string alias); + /// Id of group + /// + IEnumerable GetAllNotInGroup(int groupId); + + #region User groups /// - /// Gets a UserType by its Id + /// Gets all UserGroups or those specified as parameters /// - /// Id of the UserType to retrieve - /// - IUserType GetUserTypeById(int id); + /// Optional Ids of UserGroups to retrieve + /// An enumerable list of + IEnumerable GetAllUserGroups(params int[] ids); + + /// + /// Gets a UserGroup by its Alias + /// + /// Alias of the UserGroup to retrieve + /// + IEnumerable GetUserGroupsByAlias(params string[] alias); /// - /// Gets a UserType by its Name + /// Gets a UserGroup by its Alias /// - /// Name of the UserType to retrieve - /// - IUserType GetUserTypeByName(string name); + /// Name of the UserGroup to retrieve + /// + IUserGroup GetUserGroupByAlias(string name); /// - /// Saves a UserType + /// Gets a UserGroup by its Id /// - /// UserType to save - /// Optional parameter to raise events. + /// Id of the UserGroup to retrieve + /// + IUserGroup GetUserGroupById(int id); + + /// + /// Saves a UserGroup + /// + /// UserGroup to save + /// + /// If null than no changes are made to the users who are assigned to this group, however if a value is passed in + /// than all users will be removed from this group and only these users will be added + /// + /// Optional parameter to raise events. /// Default is True otherwise set to False to not raise events - void SaveUserType(IUserType userType, bool raiseEvents = true); + void Save(IUserGroup userGroup, int[] userIds = null, bool raiseEvents = true); /// - /// Deletes a UserType + /// Deletes a UserGroup /// - /// UserType to delete - void DeleteUserType(IUserType userType); + /// UserGroup to delete + void DeleteUserGroup(IUserGroup userGroup); #endregion } diff --git a/src/Umbraco.Core/Services/IdkMap.cs b/src/Umbraco.Core/Services/IdkMap.cs new file mode 100644 index 0000000000..3132be2c62 --- /dev/null +++ b/src/Umbraco.Core/Services/IdkMap.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence.UnitOfWork; + +namespace Umbraco.Core.Services +{ + public class IdkMap + { + private readonly IScopeUnitOfWorkProvider _uowProvider; + private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(); + private readonly Dictionary _id2Key = new Dictionary(); + private readonly Dictionary _key2Id = new Dictionary(); + + public IdkMap(IScopeUnitOfWorkProvider uowProvider) + { + _uowProvider = uowProvider; + } + + // note - no need for uow, scope would be enough, but a pain to wire + // note - for pure read-only we might want to *not* enforce a transaction? + + public Attempt GetIdForKey(Guid key, UmbracoObjectTypes umbracoObjectType) + { + int id; + try + { + _locker.EnterReadLock(); + if (_key2Id.TryGetValue(key, out id)) return Attempt.Succeed(id); + } + finally + { + if (_locker.IsReadLockHeld) + _locker.ExitReadLock(); + } + + int? val; + using (var uow = _uowProvider.CreateUnitOfWork()) + { + val = uow.Database.ExecuteScalar("SELECT id FROM umbracoNode WHERE uniqueId=@id AND nodeObjectType=@nodeObjectType", + new { id = key, nodeObjectType = GetNodeObjectTypeGuid(umbracoObjectType) }); + uow.Complete(); + } + + if (val == null) return Attempt.Fail(); + id = val.Value; + + try + { + _locker.EnterWriteLock(); + _id2Key[id] = key; + _key2Id[key] = id; + } + finally + { + if (_locker.IsWriteLockHeld) + _locker.ExitWriteLock(); + } + + return Attempt.Succeed(id); + } + + public Attempt GetIdForUdi(Udi udi) + { + var guidUdi = udi as GuidUdi; + if (guidUdi == null) + return Attempt.Fail(); + + var umbracoType = Constants.UdiEntityType.ToUmbracoObjectType(guidUdi.EntityType); + return GetIdForKey(guidUdi.Guid, umbracoType); + } + + public Attempt GetKeyForId(int id, UmbracoObjectTypes umbracoObjectType) + { + Guid key; + try + { + _locker.EnterReadLock(); + if (_id2Key.TryGetValue(id, out key)) return Attempt.Succeed(key); + } + finally + { + if (_locker.IsReadLockHeld) + _locker.ExitReadLock(); + } + + Guid? val; + using (var uow = _uowProvider.CreateUnitOfWork()) + { + val = uow.Database.ExecuteScalar("SELECT uniqueId FROM umbracoNode WHERE id=@id AND nodeObjectType=@nodeObjectType", + new { id, nodeObjectType = GetNodeObjectTypeGuid(umbracoObjectType) }); + uow.Complete(); + } + + if (val == null) return Attempt.Fail(); + key = val.Value; + + try + { + _locker.EnterWriteLock(); + _id2Key[id] = key; + _key2Id[key] = id; + } + finally + { + if (_locker.IsWriteLockHeld) + _locker.ExitWriteLock(); + } + + return Attempt.Succeed(key); + } + + private static Guid GetNodeObjectTypeGuid(UmbracoObjectTypes umbracoObjectType) + { + var guid = umbracoObjectType.GetGuid(); + if (guid == Guid.Empty) + throw new NotSupportedException("Unsupported object type (" + umbracoObjectType + ")."); + return guid; + } + + public void ClearCache() + { + try + { + _locker.EnterWriteLock(); + _id2Key.Clear(); + _key2Id.Clear(); + } + finally + { + if (_locker.IsWriteLockHeld) + _locker.ExitWriteLock(); + } + } + + public void ClearCache(int id) + { + try + { + _locker.EnterWriteLock(); + Guid key; + if (_id2Key.TryGetValue(id, out key) == false) return; + _id2Key.Remove(id); + _key2Id.Remove(key); + } + finally + { + if (_locker.IsWriteLockHeld) + _locker.ExitWriteLock(); + } + } + + public void ClearCache(Guid key) + { + try + { + _locker.EnterWriteLock(); + int id; + if (_key2Id.TryGetValue(key, out id) == false) return; + _id2Key.Remove(id); + _key2Id.Remove(key); + } + finally + { + if (_locker.IsWriteLockHeld) + _locker.ExitWriteLock(); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs index ec4abedb19..031f6dc823 100644 --- a/src/Umbraco.Core/Services/MemberService.cs +++ b/src/Umbraco.Core/Services/MemberService.cs @@ -156,6 +156,22 @@ namespace Umbraco.Core.Services return member; } + /// + /// Creates and persists a new + /// + /// An can be of type or + /// Username of the to create + /// Email of the to create + /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database + /// Alias of the Type + /// Is the member approved + /// + IMember IMembershipMemberService.CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias, bool isApproved) + { + var memberType = FindMemberTypeByAlias(memberTypeAlias); + return CreateMemberWithIdentity(username, email, username, passwordValue, memberType, isApproved); + } + /// /// Creates and persists a Member /// @@ -165,8 +181,9 @@ namespace Umbraco.Core.Services /// Email of the Member to create /// Name of the Member to create /// Alias of the MemberType the Member should be based on + /// Optional IsApproved of the Member to create /// - public IMember CreateMemberWithIdentity(string username, string email, string name, string memberTypeAlias) + public IMember CreateMemberWithIdentity(string username, string email, string name, string memberTypeAlias, bool isApproved = true) { using (var uow = UowProvider.CreateUnitOfWork()) { @@ -177,7 +194,7 @@ namespace Umbraco.Core.Services if (memberType == null) throw new ArgumentException("No member type with that alias.", nameof(memberTypeAlias)); // causes rollback - var member = new Member(name, email.ToLower().Trim(), username, memberType); + var member = new Member(name, email.ToLower().Trim(), username, memberType, isApproved); CreateMember(uow, member, 0, true); uow.Complete(); diff --git a/src/Umbraco.Core/Services/PackagingService.cs b/src/Umbraco.Core/Services/PackagingService.cs index a50ebddf5f..6b001df4ed 100644 --- a/src/Umbraco.Core/Services/PackagingService.cs +++ b/src/Umbraco.Core/Services/PackagingService.cs @@ -228,6 +228,7 @@ namespace Umbraco.Core.Services var path = element.Attribute("path").Value; //TODO: Shouldn't we be using this value??? var template = element.Attribute("template").Value; + var key = Guid.Empty; var properties = from property in element.Elements() where property.Attribute("isDoc") == null @@ -245,6 +246,12 @@ namespace Umbraco.Core.Services SortOrder = int.Parse(sortOrder) }; + if (element.Attribute("key") != null && Guid.TryParse(element.Attribute("key").Value, out key)) + { + // update the Guid (for UDI support) + content.Key = key; + } + using (var uow = _uowProvider.CreateUnitOfWork(readOnly: true)) { foreach (var property in properties) @@ -1914,12 +1921,12 @@ namespace Umbraco.Core.Services /// /// Occurs after a package is imported /// - internal static event TypedEventHandler> ImportedPackage; + public static event TypedEventHandler> ImportedPackage; /// /// Occurs after a package is uninstalled /// - internal static event TypedEventHandler> UninstalledPackage; + public static event TypedEventHandler> UninstalledPackage; #endregion } diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 000d5e0eda..164abe444c 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -1,13 +1,20 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Data.Common; +using System.Data.SqlClient; +using System.Data.SqlServerCe; +using System.Globalization; using System.Linq; +using System.Linq.Expressions; using Umbraco.Core.Configuration; using Umbraco.Core.Events; using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; +using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.UnitOfWork; @@ -33,35 +40,6 @@ namespace Umbraco.Core.Services #region Implementation of IMembershipUserService - /// - /// Gets the default MemberType alias - /// - /// By default we'll return the 'writer', but we need to check it exists. If it doesn't we'll - /// return the first type that is not an admin, otherwise if there's only one we will return that one. - /// Alias of the default MemberType - public string GetDefaultMemberType() - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - - var types = repository.GetAll().Select(x => x.Alias).ToArray(); - - if (types.Any() == false) - throw new EntityNotFoundException("No member types could be resolved"); // causes rollback - - // anything else is ok - uow.Complete(); - - if (types.InvariantContains("writer")) - return types.First(x => x.InvariantEquals("writer")); - - return types.Length == 1 - ? types[0] // there's only one - : types.First(x => x.InvariantEquals("admin") == false); // first that is not admin - } - } - /// /// Checks if a User with the username exists /// @@ -82,11 +60,10 @@ namespace Umbraco.Core.Services /// The user will be saved in the database and returned with an Id /// Username of the user to create /// Email of the user to create - /// which the User should be based on /// - public IUser CreateUserWithIdentity(string username, string email, IUserType userType) + public IUser CreateUserWithIdentity(string username, string email) { - return CreateUserWithIdentity(username, email, "", userType); + return CreateUserWithIdentity(username, email, string.Empty); } /// @@ -95,15 +72,24 @@ namespace Umbraco.Core.Services /// Username of the to create /// Email of the to create /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database - /// Alias of the Type + /// Not used for users /// IUser IMembershipMemberService.CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias) { - var userType = GetUserTypeByAlias(memberTypeAlias); - if (userType == null) - throw new EntityNotFoundException("The user type " + memberTypeAlias + " could not be resolved"); + return CreateUserWithIdentity(username, email, passwordValue); - return CreateUserWithIdentity(username, email, passwordValue, userType); + /// + /// Creates and persists a new + /// + /// Username of the to create + /// Email of the to create + /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database + /// Alias of the Type + /// Is the member approved + /// + IUser IMembershipMemberService.CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias, bool isApproved) + { + return CreateUserWithIdentity(username, email, passwordValue, isApproved); } /// @@ -114,11 +100,10 @@ namespace Umbraco.Core.Services /// Username of the Member to create /// Email of the Member to create /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database - /// MemberType the Member should be based on + /// Is the user approved /// - private IUser CreateUserWithIdentity(string username, string email, string passwordValue, IUserType userType) + private IUser CreateUserWithIdentity(string username, string email, string passwordValue, bool isApproved = true) { - if (userType == null) throw new ArgumentNullException(nameof(userType)); if (string.IsNullOrWhiteSpace(username)) throw new ArgumentNullOrEmptyException(nameof(username)); //TODO: PUT lock here!! @@ -131,7 +116,7 @@ namespace Umbraco.Core.Services if (loginExists) throw new ArgumentException("Login already exists"); // causes rollback - user = new User(userType) + user = new User { DefaultToLiveEditing = false, Email = email, @@ -139,17 +124,10 @@ namespace Umbraco.Core.Services Name = username, RawPasswordValue = passwordValue, Username = username, - StartContentId = -1, - StartMediaId = -1, IsLockedOut = false, - IsApproved = true + IsApproved = isApproved }; - //adding default sections content, media + translation - user.AddAllowedSection(Constants.Applications.Content); - user.AddAllowedSection(Constants.Applications.Media); - user.AddAllowedSection(Constants.Applications.Translation); - if (uow.Events.DispatchCancelable(SavingUser, this, new SaveEventArgs(user))) { uow.Complete(); @@ -215,8 +193,29 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); - var query = uow.Query().Where(x => x.Username.Equals(username)); - return repository.GetByQuery(query).FirstOrDefault(); + + try + { + return repository.GetByUsername(username, includeSecurityData: true); + } + catch (Exception ex) + { + if (ex is SqlException || ex is SqlCeException) + { + // fixme kill in v8 + //we need to handle this one specific case which is when we are upgrading to 7.7 since the user group + //tables don't exist yet. This is the 'easiest' way to deal with this without having to create special + //version checks in the BackOfficeSignInManager and calling into other special overloads that we'd need + //like "GetUserById(int id, bool includeSecurityData)" which may cause confusion because the result of + //that method would not be cached. + if (ApplicationContext.Current.IsUpgrading) + { + //NOTE: this will not be cached + return repository.GetByUsername(username, includeSecurityData: false); + } + } + throw; + } } } @@ -228,22 +227,12 @@ namespace Umbraco.Core.Services { //disable membershipUser.IsApproved = false; - //can't rename if it's going to take up too many chars - if (membershipUser.Username.Length + 9 <= 125) - { - membershipUser.Username = DateTime.Now.ToString("yyyyMMdd") + "_" + membershipUser.Username; - } + Save(membershipUser); } - /// - /// This is simply a helper method which essentially just wraps the MembershipProvider's ChangePassword method - /// - /// - /// This method exists so that Umbraco developers can use one entry point to create/update users if they choose to. - /// - /// The user to save the password for - /// The password to save + [Obsolete("ASP.NET Identity APIs like the BackOfficeUserManager should be used to manage passwords, this will not work with correct security practices because you would need the existing password")] + [EditorBrowsable(EditorBrowsableState.Never)] public void SavePassword(IUser user, string password) { if (user == null) throw new ArgumentNullException(nameof(user)); @@ -319,6 +308,18 @@ namespace Umbraco.Core.Services throw new ArgumentException("Empty name.", nameof(entity)); var repository = uow.CreateRepository(); + + //Now we have to check for backwards compat hacks + var explicitUser = entity as User; + if (explicitUser != null && explicitUser.GroupsToSave.Count > 0) + { + var groupRepository = uow.CreateRepository(); + foreach (var userGroup in explicitUser.GroupsToSave) + { + groupRepository.AddOrUpdate(userGroup); + } + } + try { repository.AddOrUpdate(entity); @@ -366,15 +367,26 @@ namespace Umbraco.Core.Services } var repository = uow.CreateRepository(); - foreach (var member in entitiesA) + var groupRepository = uow.CreateRepository(); + foreach (var user in entitiesA) { - if (string.IsNullOrWhiteSpace(member.Username)) + if (string.IsNullOrWhiteSpace(user.Username)) throw new ArgumentException("Empty username.", nameof(entities)); - if (string.IsNullOrWhiteSpace(member.Name)) + if (string.IsNullOrWhiteSpace(user.Name)) throw new ArgumentException("Empty name.", nameof(entities)); - repository.AddOrUpdate(member); + repository.AddOrUpdate(user); + + //Now we have to check for backwards compat hacks + var explicitUser = user as User; + if (explicitUser != null && explicitUser.GroupsToSave.Count > 0) + { + foreach (var userGroup in explicitUser.GroupsToSave) + { + groupRepository.AddOrUpdate(userGroup); + } + } } if (raiseEvents) @@ -384,6 +396,14 @@ namespace Umbraco.Core.Services uow.Complete(); } } + /// + /// This is just the default user group that the membership provider will use + /// + /// + public string GetDefaultMemberType() + { + return "writer"; + } /// /// Finds a list of objects by a partial email string @@ -512,6 +532,68 @@ namespace Umbraco.Core.Services } } + public IDictionary GetUserStates() + { + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + var repository = uow.CreateRepository(); + return repository.GetUserStates(); + } + } + + public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, UserState[] userState = null, string[] userGroups = null, string filter = "") + { + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + Expression> sort; + switch (orderBy.ToUpperInvariant()) + { + case "USERNAME": + sort = member => member.Username; + break; + case "LANGUAGE": + sort = member => member.Language; + break; + case "NAME": + sort = member => member.Name; + break; + case "EMAIL": + sort = member => member.Email; + break; + case "ID": + sort = member => member.Id; + break; + case "CREATEDATE": + sort = member => member.CreateDate; + break; + case "UPDATEDATE": + sort = member => member.UpdateDate; + break; + case "ISAPPROVED": + sort = member => member.IsApproved; + break; + case "ISLOCKEDOUT": + sort = member => member.IsLockedOut; + break; + case "LASTLOGINDATE": + sort = member => member.LastLoginDate; + break; + default: + throw new IndexOutOfRangeException("The orderBy parameter " + orderBy + " is not valid"); + } + + IQuery filterQuery = null; + if (filter.IsNullOrWhiteSpace() == false) + { +#error query + filterQuery = Query.Builder.Where(x => x.Name.Contains(filter) || x.Username.Contains(filter)); + } + + var repository = uow.CreateRepository(); + return repository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, sort, orderDirection, userGroups, userState, filterQuery); + } + } + /// /// Gets a list of paged objects /// @@ -537,6 +619,34 @@ namespace Umbraco.Core.Services } } + /// + /// Gets a list of objects associated with a given group + /// + /// Id of group + /// + public IEnumerable GetAllInGroup(int groupId) + { + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + var repository = uow.CreateRepository(); + return repository.GetAllInGroup(groupId); + } + } + + /// + /// Gets a list of objects not associated with a given group + /// + /// Id of group + /// + public IEnumerable GetAllNotInGroup(int groupId) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repository = uow.CreateRepository(); + return repository.GetAllNotInGroup(groupId); + } + } + #endregion #region Implementation of IUserService @@ -548,8 +658,11 @@ namespace Umbraco.Core.Services /// public IProfile GetProfileById(int id) { - var user = GetUserById(id); - return user.ProfileData; + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + var repository = uow.CreateRepository(); + return repository.GetProfile(id); + } } /// @@ -559,8 +672,11 @@ namespace Umbraco.Core.Services /// public IProfile GetProfileByUserName(string username) { - var user = GetByUsername(username); - return user.ProfileData; + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = uow.CreateRepository(); + return repository.GetProfile(username); + } } /// @@ -573,145 +689,204 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); - return repository.Get(id); + try + { + return repository.Get(id); + } + catch (Exception ex) + { + if (ex is SqlException || ex is SqlCeException) + { + // fixme kill in v8 + //we need to handle this one specific case which is when we are upgrading to 7.7 since the user group + //tables don't exist yet. This is the 'easiest' way to deal with this without having to create special + //version checks in the BackOfficeSignInManager and calling into other special overloads that we'd need + //like "GetUserById(int id, bool includeSecurityData)" which may cause confusion because the result of + //that method would not be cached. + if (ApplicationContext.Current.IsUpgrading) + { + //NOTE: this will not be cached + return repository.Get(id, includeSecurityData: false); + } + } + throw; + } } } - /// - /// Replaces the same permission set for a single user to any number of entities - /// - /// If no 'entityIds' are specified all permissions will be removed for the specified user. - /// Id of the user - /// Permissions as enumerable list of - /// Specify the nodes to replace permissions for. If nothing is specified all permissions are removed. - public void ReplaceUserPermissions(int userId, IEnumerable permissions, params int[] entityIds) + public IEnumerable GetUsersById(params int[] ids) { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - repository.ReplaceUserPermissions(userId, permissions, entityIds); - uow.Complete(); - } - } + if (ids.Length <= 0) return Enumerable.Empty(); - /// - /// Assigns the same permission set for a single user to any number of entities - /// - /// Id of the user - /// - /// Specify the nodes to replace permissions for - public void AssignUserPermission(int userId, char permission, params int[] entityIds) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - repository.AssignUserPermission(userId, permission, entityIds); - uow.Complete(); - } - } - - /// - /// Gets all UserTypes or thosed specified as parameters - /// - /// Optional Ids of UserTypes to retrieve - /// An enumerable list of - public IEnumerable GetAllUserTypes(params int[] ids) - { using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { - var repository = uow.CreateRepository(); + var repository = uow.CreateRepository(); return repository.GetAll(ids); } } /// - /// Gets a UserType by its Alias + /// Replaces the same permission set for a single group to any number of entities /// - /// Alias of the UserType to retrieve - /// - public IUserType GetUserTypeByAlias(string alias) + /// If no 'entityIds' are specified all permissions will be removed for the specified group. + /// Id of the group + /// Permissions as enumerable list of If nothing is specified all permissions are removed. + /// Specify the nodes to replace permissions for. + public void ReplaceUserGroupPermissions(int groupId, IEnumerable permissions, params int[] entityIds) { - using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + if (entityIds.Length == 0) + return; + + using (var uow = UowProvider.CreateUnitOfWork()) { - var repository = uow.CreateRepository(); - var query = uow.Query().Where(x => x.Alias == alias); - return repository.GetByQuery(query).SingleOrDefault(); + var repository = uow.CreateRepository(); + repository.ReplaceGroupPermissions(groupId, permissions, entityIds); + uow.Complete(); + + uow.Events.Dispatch(UserGroupPermissionsAssigned, this, new SaveEventArgs( + entityIds.Select( + x => new EntityPermission( + groupId, + x, + permissions.Select(p => p.ToString(CultureInfo.InvariantCulture)).ToArray())) + .ToArray(), false)); } } /// - /// Gets a UserType by its Id + /// Assigns the same permission set for a single user group to any number of entities /// - /// Id of the UserType to retrieve - /// - public IUserType GetUserTypeById(int id) + /// Id of the user group + /// + /// Specify the nodes to replace permissions for + public void AssignUserGroupPermission(int groupId, char permission, params int[] entityIds) + { + if (entityIds.Length == 0) + return; + + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repository = uow.CreateRepository(); + repository.AssignGroupPermission(groupId, permission, entityIds); + uow.Complete(); + + uow.Events.Dispatch(UserGroupPermissionsAssigned, this, new SaveEventArgs( + entityIds.Select( + x => new EntityPermission( + groupId, + x, + new[] {permission.ToString(CultureInfo.InvariantCulture)})) + .ToArray(), false)); + } + } + + /// + /// Gets all UserGroups or those specified as parameters + /// + /// Optional Ids of UserGroups to retrieve + /// An enumerable list of + public IEnumerable GetAllUserGroups(params int[] ids) { using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { - var repository = uow.CreateRepository(); + var repository = uow.CreateRepository(); + return repository.GetAll(ids).OrderBy(x => x.Name); + } + } + + public IEnumerable GetUserGroupsByAlias(params string[] aliases) + { + if (aliases.Length == 0) return Enumerable.Empty(); + + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + var repository = uow.CreateRepository(); + var query = uow.Query().Where(x => aliases.SqlIn(x.Alias)); + var contents = repository.GetByQuery(query); + return contents.ToArray(); + } + } + + /// + /// Gets a UserGroup by its Alias + /// + /// Alias of the UserGroup to retrieve + /// + public IUserGroup GetUserGroupByAlias(string alias) + { + if (string.IsNullOrWhiteSpace(alias)) throw new ArgumentException("Value cannot be null or whitespace.", "alias"); + + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + var repository = uow.CreateRepository(); + var query = uow.Query().Where(x => x.Alias == alias); + var contents = repository.GetByQuery(query); + return contents.FirstOrDefault(); + } + } + + /// + /// Gets a UserGroup by its Id + /// + /// Id of the UserGroup to retrieve + /// + public IUserGroup GetUserGroupById(int id) + { + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + var repository = uow.CreateRepository(); return repository.Get(id); } } /// - /// Gets a UserType by its Name + /// Saves a UserGroup /// - /// Name of the UserType to retrieve - /// - public IUserType GetUserTypeByName(string name) - { - using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) - { - var repository = uow.CreateRepository(); - var query = uow.Query().Where(x => x.Name == name); - return repository.GetByQuery(query).SingleOrDefault(); - } - } - - /// - /// Saves a UserType - /// - /// UserType to save + /// UserGroup to save + /// + /// If null than no changes are made to the users who are assigned to this group, however if a value is passed in + /// than all users will be removed from this group and only these users will be added + /// /// Optional parameter to raise events. /// Default is True otherwise set to False to not raise events - public void SaveUserType(IUserType userType, bool raiseEvents = true) + public void Save(IUserGroup userGroup, int[] userIds = null, bool raiseEvents = true) { using (var uow = UowProvider.CreateUnitOfWork()) { - if (raiseEvents && uow.Events.DispatchCancelable(SavingUserType, this, new SaveEventArgs(userType))) + if (raiseEvents && uow.Events.DispatchCancelable(SavingUserGroup, this, new SaveEventArgs(userGroup))) { uow.Complete(); return; } - var repository = uow.CreateRepository(); - repository.AddOrUpdate(userType); + var repository = uow.CreateRepository(); + repository.AddOrUpdateGroupWithUsers(userGroup, userIds); if (raiseEvents) - uow.Events.Dispatch(SavedUserType, this, new SaveEventArgs(userType, false)); + uow.Events.Dispatch(SavedUserGroup, this, new SaveEventArgs(userGroup, false)); uow.Complete(); } } /// - /// Deletes a UserType + /// Deletes a UserGroup /// - /// UserType to delete - public void DeleteUserType(IUserType userType) + /// UserGroup to delete + public void DeleteUserGroup(IUserGroup userGroup) { using (var uow = UowProvider.CreateUnitOfWork()) { - if (uow.Events.DispatchCancelable(DeletingUserType, this, new DeleteEventArgs(userType))) + if (uow.Events.DispatchCancelable(DeletingUserGroup, this, new DeleteEventArgs(userGroup))) { uow.Complete(); return; } - var repository = uow.CreateRepository(); - repository.Delete(userType); + var repository = uow.CreateRepository(); + repository.Delete(userGroup); - uow.Events.Dispatch(DeletedUserType, this, new DeleteEventArgs(userType, false)); + uow.Events.Dispatch(DeletedUserGroup, this, new DeleteEventArgs(userGroup, false)); uow.Complete(); } @@ -722,69 +897,275 @@ namespace Umbraco.Core.Services /// /// This is useful when an entire section is removed from config /// Alias of the section to remove - public void DeleteSectionFromAllUsers(string sectionAlias) + public void DeleteSectionFromAllUserGroups(string sectionAlias) { using (var uow = UowProvider.CreateUnitOfWork()) { - var repository = uow.CreateRepository(); - var assignedUsers = repository.GetUsersAssignedToSection(sectionAlias); - foreach (var user in assignedUsers) + var repository = uow.CreateRepository(); + var assignedGroups = repository.GetGroupsAssignedToSection(sectionAlias); + foreach (var group in assignedGroups) { //now remove the section for each user and commit - user.RemoveAllowedSection(sectionAlias); - repository.AddOrUpdate(user); + group.RemoveAllowedSection(sectionAlias); + repository.AddOrUpdate(group); } + uow.Complete(); } } /// - /// Add a specific section to all users or those specified as parameters + /// Get explicitly assigned permissions for a user and optional node ids /// - /// This is useful when a new section is created to allow specific users accessing it - /// Alias of the section to add - /// Specifiying nothing will add the section to all user - public void AddSectionToAllUsers(string sectionAlias, params int[] userIds) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - var users = userIds.Any() ? repository.GetAll(userIds) : repository.GetAll(); - foreach (var user in users.Where(u => u.AllowedSections.InvariantContains(sectionAlias) == false)) - { - //now add the section for each user and commit - user.AddAllowedSection(sectionAlias); - repository.AddOrUpdate(user); - } - uow.Complete(); - } - } - - /// - /// Get permissions set for a user and optional node ids - /// - /// If no permissions are found for a particular entity then the user's default permissions will be applied /// User to retrieve permissions for - /// Specifiying nothing will return all user permissions for all nodes + /// Specifiying nothing will return all permissions for all nodes /// An enumerable list of - public IEnumerable GetPermissions(IUser user, params int[] nodeIds) + public EntityPermissionCollection GetPermissions(IUser user, params int[] nodeIds) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + var repository = uow.CreateRepository(); + return repository.GetPermissions(user.Groups.ToArray(), true, nodeIds); + } + } + + /// + /// Get explicitly assigned permissions for a group and optional node Ids + /// + /// Groups to retrieve permissions for + /// + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set + /// + /// Specifiying nothing will return all permissions for all nodes + /// An enumerable list of + private IEnumerable GetPermissions(IReadOnlyUserGroup[] groups, bool fallbackToDefaultPermissions, params int[] nodeIds) + { + if (groups == null) throw new ArgumentNullException(nameof(groups)); + + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + var repository = uow.CreateRepository(); + return repository.GetPermissions(groups, fallbackToDefaultPermissions, nodeIds); + } + } + + /// + /// Get explicitly assigned permissions for a group and optional node Ids + /// + /// + /// + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set + /// + /// Specifiying nothing will return all permissions for all nodes + /// An enumerable list of + public EntityPermissionCollection GetPermissions(IUserGroup[] groups, bool fallbackToDefaultPermissions, params int[] nodeIds) + { + if (groups == null) throw new ArgumentNullException(nameof(groups)); + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); - var explicitPermissions = repository.GetUserPermissionsForEntities(user.Id, nodeIds); + return repository.GetPermissions(groups.Select(x => x.ToReadOnlyGroup()).ToArray(), fallbackToDefaultPermissions, nodeIds); + } + } + /// + /// Gets the implicit/inherited permissions for the user for the given path + /// + /// User to check permissions for + /// Path to check permissions for + public EntityPermissionSet GetPermissionsForPath(IUser user, string path) + { + var nodeIds = path.GetIdsFromPathReversed(); - //if no permissions are assigned to a particular node then we will fill in those permissions with the user's defaults - var result = new List(explicitPermissions); - var missingIds = nodeIds.Except(result.Select(x => x.EntityId)); - foreach (var id in missingIds) + if (nodeIds.Length == 0) + return EntityPermissionSet.Empty(); + + //collect all permissions structures for all nodes for all groups belonging to the user + var groupPermissions = GetPermissionsForPath(user.Groups.ToArray(), nodeIds, fallbackToDefaultPermissions: true).ToArray(); + + return CalculatePermissionsForPathForUser(groupPermissions, nodeIds); + } + + /// + /// Gets the permissions for the provided group and path + /// + /// + /// Path to check permissions for + /// + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set + /// + /// String indicating permissions for provided user and path + public EntityPermissionSet GetPermissionsForPath(IUserGroup[] groups, string path, bool fallbackToDefaultPermissions = false) + { + var nodeIds = path.GetIdsFromPathReversed(); + + if (nodeIds.Length == 0) + return EntityPermissionSet.Empty(); + + //collect all permissions structures for all nodes for all groups + var groupPermissions = GetPermissionsForPath(groups.Select(x => x.ToReadOnlyGroup()).ToArray(), nodeIds, fallbackToDefaultPermissions: true).ToArray(); + + return CalculatePermissionsForPathForUser(groupPermissions, nodeIds); + } + + private EntityPermissionCollection GetPermissionsForPath(IReadOnlyUserGroup[] groups, int[] pathIds, bool fallbackToDefaultPermissions = false) + { + if (pathIds.Length == 0) + return new EntityPermissionCollection(Enumerable.Empty()); + + //get permissions for all nodes in the path by group + var permissions = GetPermissions(groups, fallbackToDefaultPermissions, pathIds) + .GroupBy(x => x.UserGroupId); + + return new EntityPermissionCollection( + permissions.Select(x => GetPermissionsForPathForGroup(x, pathIds, fallbackToDefaultPermissions))); + } + + /// + /// This performs the calculations for inherited nodes based on this http://issues.umbraco.org/issue/U4-10075#comment=67-40085 + /// + /// + /// + /// + internal static EntityPermissionSet CalculatePermissionsForPathForUser( + EntityPermission[] groupPermissions, + int[] pathIds) + { + // not sure this will ever happen, it shouldn't since this should return defaults, but maybe those are empty? + if (groupPermissions.Length == 0 || pathIds.Length == 0) + return EntityPermissionSet.Empty(); + + //The actual entity id being looked at (deepest part of the path) + var entityId = pathIds[0]; + + var resultPermissions = new EntityPermissionCollection(); + + //create a grouped by dictionary of another grouped by dictionary + var permissionsByGroup = groupPermissions + .GroupBy(x => x.UserGroupId) + .ToDictionary( + x => x.Key, + x => x.GroupBy(a => a.EntityId).ToDictionary(a => a.Key, a => a.ToArray())); + + //iterate through each group + foreach (var byGroup in permissionsByGroup) + { + var added = false; + + //iterate deepest to shallowest + foreach (var pathId in pathIds) { - result.Add(new EntityPermission(user.Id, id, user.DefaultPermissions.ToArray())); + EntityPermission[] permissionsForNodeAndGroup; + if (byGroup.Value.TryGetValue(pathId, out permissionsForNodeAndGroup) == false) + continue; + + //In theory there will only be one EntityPermission in this group + // but there's nothing stopping the logic of this method + // from having more so we deal with it here + foreach (var entityPermission in permissionsForNodeAndGroup) + { + if (entityPermission.IsDefaultPermissions == false) + { + //explicit permision found so we'll append it and move on, the collection is a hashset anyways + //so only supports adding one element per groupid/contentid + resultPermissions.Add(entityPermission); + added = true; + break; + } + } + + //if the permission has been added for this group and this branch then we can exit this loop + if (added) + break; + } + + if (added == false && byGroup.Value.Count > 0) + { + //if there was no explicit permissions assigned in this branch for this group, then we will + //add the group's default permissions + resultPermissions.Add(byGroup.Value[entityId][0]); } - uow.Complete(); - return result; } + + var permissionSet = new EntityPermissionSet(entityId, resultPermissions); + return permissionSet; + } + + /// + /// Returns the resulting permission set for a group for the path based on all permissions provided for the branch + /// + /// + /// The collective set of permissions provided to calculate the resulting permissions set for the path + /// based on a single group + /// + /// Must be ordered deepest to shallowest (right to left) + /// + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set + /// + /// + internal static EntityPermission GetPermissionsForPathForGroup( + IEnumerable pathPermissions, + int[] pathIds, + bool fallbackToDefaultPermissions = false) + { + //get permissions for all nodes in the path + var permissionsByEntityId = pathPermissions.ToDictionary(x => x.EntityId, x => x); + + //then the permissions assigned to the path will be the 'deepest' node found that has permissions + foreach (var id in pathIds) + { + EntityPermission permission; + if (permissionsByEntityId.TryGetValue(id, out permission)) + { + //don't return the default permissions if that is the one assigned here (we'll do that below if nothing was found) + if (permission.IsDefaultPermissions == false) + return permission; + } + } + + //if we've made it here it means that no implicit/inherited permissions were found so we return the defaults if that is specified + if (fallbackToDefaultPermissions == false) + return null; + + return permissionsByEntityId[pathIds[0]]; + } + + /// + /// Checks in a set of permissions associated with a user for those related to a given nodeId + /// + /// The set of permissions + /// The node Id + /// The permissions to return + /// True if permissions for the given path are found + public static bool TryGetAssignedPermissionsForNode(IList permissions, + int nodeId, + out string assignedPermissions) + { + if (permissions.Any(x => x.EntityId == nodeId)) + { + var found = permissions.First(x => x.EntityId == nodeId); + var assignedPermissionsArray = found.AssignedPermissions.ToList(); + + // Working with permissions assigned directly to a user AND to their groups, so maybe several per node + // and we need to get the most permissive set + foreach (var permission in permissions.Where(x => x.EntityId == nodeId).Skip(1)) + { + AddAdditionalPermissions(assignedPermissionsArray, permission.AssignedPermissions); + } + + assignedPermissions = string.Join("", assignedPermissionsArray); + return true; + } + + assignedPermissions = string.Empty; + return false; + } + + private static void AddAdditionalPermissions(List assignedPermissions, string[] additionalPermissions) + { + var permissionsToAdd = additionalPermissions + .Where(x => assignedPermissions.Contains(x) == false); + assignedPermissions.AddRange(permissionsToAdd); } #endregion @@ -812,21 +1193,25 @@ namespace Umbraco.Core.Services /// /// Occurs before Save /// - public static event TypedEventHandler> SavingUserType; + public static event TypedEventHandler> SavingUserGroup; /// /// Occurs after Save /// - public static event TypedEventHandler> SavedUserType; + public static event TypedEventHandler> SavedUserGroup; /// /// Occurs before Delete /// - public static event TypedEventHandler> DeletingUserType; + public static event TypedEventHandler> DeletingUserGroup; /// /// Occurs after Delete /// - public static event TypedEventHandler> DeletedUserType; + public static event TypedEventHandler> DeletedUserGroup; + + //TODO: still don't know if we need this yet unless we start caching permissions, but that also means we'll need another + // event on the ContentService since there's a method there to modify node permissions too, or we can proxy events if needed. + internal static event TypedEventHandler> UserGroupPermissionsAssigned; } } diff --git a/src/Umbraco.Core/Services/UserServiceExtensions.cs b/src/Umbraco.Core/Services/UserServiceExtensions.cs index 0d7b53ca82..7e392ffb39 100644 --- a/src/Umbraco.Core/Services/UserServiceExtensions.cs +++ b/src/Umbraco.Core/Services/UserServiceExtensions.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Models.Membership; namespace Umbraco.Core.Services { - internal static class UserServiceExtensions + public static class UserServiceExtensions { public static EntityPermission GetPermissions(this IUserService userService, IUser user, string path) { @@ -20,24 +20,53 @@ namespace Umbraco.Core.Services } /// - /// Remove all permissions for this user for all nodes specified + /// Get explicitly assigned permissions for a group and optional node Ids /// - /// - /// - /// - public static void RemoveUserPermissions(this IUserService userService, int userId, params int[] entityIds) + /// + /// + /// + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set + /// + /// Specifiying nothing will return all permissions for all nodes + /// An enumerable list of + public static EntityPermissionCollection GetPermissions(this IUserService service, IUserGroup group, bool fallbackToDefaultPermissions, params int[] nodeIds) { - userService.ReplaceUserPermissions(userId, new char[] {}, entityIds); + return service.GetPermissions(new[] {group}, fallbackToDefaultPermissions, nodeIds); } /// - /// Remove all permissions for this user for all nodes + /// Gets the permissions for the provided group and path + /// + /// + /// + /// Path to check permissions for + /// + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set + /// + public static EntityPermissionSet GetPermissionsForPath(this IUserService service, IUserGroup group, string path, bool fallbackToDefaultPermissions = false) + { + return service.GetPermissionsForPath(new[] { group }, path, fallbackToDefaultPermissions); + } + + /// + /// Remove all permissions for this user group for all nodes specified /// /// - /// - public static void RemoveUserPermissions(this IUserService userService, int userId) + /// + /// + public static void RemoveUserGroupPermissions(this IUserService userService, int groupId, params int[] entityIds) { - userService.ReplaceUserPermissions(userId, new char[] { }); + userService.ReplaceUserGroupPermissions(groupId, new char[] {}, entityIds); + } + + /// + /// Remove all permissions for this user group for all nodes + /// + /// + /// + public static void RemoveUserGroupPermissions(this IUserService userService, int groupId) + { + userService.ReplaceUserGroupPermissions(groupId, new char[] { }); } /// @@ -49,7 +78,7 @@ namespace Umbraco.Core.Services /// To maintain compatibility we have to check the login name if the provider key lookup fails but otherwise /// we'll store the provider user key in the login column. /// - public static IUser CreateUserMappingForCustomProvider(this IUserService userService, MembershipUser member) + internal static IUser CreateUserMappingForCustomProvider(this IUserService userService, MembershipUser member) { if (member == null) throw new ArgumentNullException("member"); @@ -64,18 +93,11 @@ namespace Umbraco.Core.Services if (found == null) { - var writer = userService.GetUserTypeByAlias("writer"); - if (writer == null) - { - throw new InvalidOperationException("Could not map the custom user to an Umbraco user, no 'writer' user type could be found"); - } var user = new User( member.UserName, member.Email ?? Guid.NewGuid().ToString("N") + "@example.com", //email cannot be empty member.ProviderUserKey == null ? member.UserName : member.ProviderUserKey.ToString(), - Guid.NewGuid().ToString("N"), //pass cannot be empty - writer); - user.AddAllowedSection(Constants.Applications.Content); + Guid.NewGuid().ToString("N")); //pass cannot be empty userService.Save(user); return user; } diff --git a/src/Umbraco.Web/Services/SectionService.cs b/src/Umbraco.Web/Services/SectionService.cs index aa11eb118f..56ceda5279 100644 --- a/src/Umbraco.Web/Services/SectionService.cs +++ b/src/Umbraco.Web/Services/SectionService.cs @@ -34,13 +34,10 @@ namespace Umbraco.Web.Services IScopeUnitOfWorkProvider uowProvider, CacheHelper cache) { - if (applicationTreeService == null) throw new ArgumentNullException("applicationTreeService"); - if (cache == null) throw new ArgumentNullException("cache"); - + _applicationTreeService = applicationTreeService ?? throw new ArgumentNullException(nameof(applicationTreeService)); + _cache = cache ?? throw new ArgumentNullException(nameof(cache)); _userService = userService; - _applicationTreeService = applicationTreeService; _uowProvider = uowProvider; - _cache = cache; _allAvailableSections = new Lazy>(() => new LazyEnumerableSections()); } @@ -61,7 +58,7 @@ namespace Umbraco.Web.Services } return _appConfig; } - set { _appConfig = value; } + set => _appConfig = value; } /// @@ -177,7 +174,9 @@ namespace Umbraco.Web.Services /// The application icon, which has to be located in umbraco/images/tray folder. public void MakeNew(string name, string alias, string icon) { - MakeNew(name, alias, icon, GetSections().Max(x => x.SortOrder) + 1); + var sections = GetSections(); + var nextSortOrder = sections.Any() ? sections.Max(x => x.SortOrder) + 1 : 1; + MakeNew(name, alias, icon, nextSortOrder); } /// @@ -216,7 +215,7 @@ namespace Umbraco.Web.Services //delete the assigned applications using (var uow = _uowProvider.CreateUnitOfWork()) { - uow.Database.Execute("delete from umbracoUser2App where app = @appAlias", + uow.Database.Execute("delete from umbracoUserGroup2App where app = @appAlias", new { appAlias = section.Alias }); uow.Complete(); }