diff --git a/src/Umbraco.Core/Models/Membership/IUser.cs b/src/Umbraco.Core/Models/Membership/IUser.cs index b7afdeb624..2a1d904ecc 100644 --- a/src/Umbraco.Core/Models/Membership/IUser.cs +++ b/src/Umbraco.Core/Models/Membership/IUser.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Models.Membership @@ -16,7 +17,11 @@ namespace Umbraco.Core.Models.Membership int SessionTimeout { get; set; } int[] StartContentIds { get; set; } int[] StartMediaIds { get; set; } - string Language { get; set; } + string Language { get; set; } + + [Obsolete("This should not be used it exists for legacy reasons only, use user groups instead, it will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] + IUserType UserType { get; set; } DateTime? EmailConfirmedDate { get; set; } DateTime? InvitedDate { get; set; } @@ -32,6 +37,14 @@ namespace Umbraco.Core.Models.Membership IEnumerable AllowedSections { get; } + [Obsolete("This should not be used it exists for legacy reasons only, use user groups instead, it will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] + void RemoveAllowedSection(string sectionAlias); + + [Obsolete("This should not be used it exists for legacy reasons only, use user groups instead, it will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] + void AddAllowedSection(string sectionAlias); + /// /// Exposes the basic profile data /// diff --git a/src/Umbraco.Core/Models/Membership/IUserType.cs b/src/Umbraco.Core/Models/Membership/IUserType.cs new file mode 100644 index 0000000000..ba004cea4a --- /dev/null +++ b/src/Umbraco.Core/Models/Membership/IUserType.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Models.Membership +{ + [Obsolete("This should not be used it exists for legacy reasons only, use user groups instead, it will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] + public interface IUserType : IAggregateRoot + { + string Alias { get; set; } + string Name { get; set; } + IEnumerable Permissions { get; set; } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index b64fd07452..253f270810 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -2,10 +2,12 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; +using System.ComponentModel; using System.Linq; using System.Reflection; using System.Runtime.Serialization; using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Models.Membership @@ -162,7 +164,7 @@ namespace Umbraco.Core.Models.Membership get { return Id; } set { throw new NotSupportedException("Cannot set the provider user key for a user"); } } - + [DataMember] public DateTime? EmailConfirmedDate { @@ -278,6 +280,165 @@ namespace Umbraco.Core.Models.Membership get { return _allowedSections ?? (_allowedSections = new List(_userGroups.SelectMany(x => x.AllowedSections).Distinct())); } } + [Obsolete("This should not be used it exists for legacy reasons only, use user groups instead, it will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] + IUserType IUser.UserType + { + get + { + //the best we can do here is to return the user's first user group as a IUserType object + //but we should attempt to return any group that is the built in ones first + var groups = Groups.ToArray(); + if (groups.Length == 0) + { + //In backwards compatibility land, a user type cannot be null! so we need to return a fake one. + return new UserType + { + Alias = "temp", + Id = int.MinValue, + Key = Guid.Empty, + CreateDate = default(DateTime), + DeletedDate = null, + Name = "Temp", + Permissions = new List(), + UpdateDate = default(DateTime) + }; + } + var builtIns = new[] { Constants.Security.AdminGroupAlias, "writer", "editor", "translator" }; + var foundBuiltIn = groups.FirstOrDefault(x => builtIns.Contains(x.Alias)); + IUserGroup realGroup; + if (foundBuiltIn != null) + { + //if the group isn't IUserGroup we'll need to look it up + realGroup = foundBuiltIn as IUserGroup ?? ApplicationContext.Current.Services.UserService.GetUserGroupById(foundBuiltIn.Id); + + //return a mapped version of the group + return new UserType + { + Alias = realGroup.Alias, + Id = realGroup.Id, + Key = realGroup.Key, + CreateDate = realGroup.CreateDate, + DeletedDate = realGroup.DeletedDate, + Name = realGroup.Name, + Permissions = realGroup.Permissions, + UpdateDate = realGroup.UpdateDate + }; + } + + //otherwise return the first + //if the group isn't IUserGroup we'll need to look it up + realGroup = groups[0] as IUserGroup ?? ApplicationContext.Current.Services.UserService.GetUserGroupById(groups[0].Id); + //return a mapped version of the group + return new UserType + { + Alias = realGroup.Alias, + Id = realGroup.Id, + Key = realGroup.Key, + CreateDate = realGroup.CreateDate, + DeletedDate = realGroup.DeletedDate, + Name = realGroup.Name, + Permissions = realGroup.Permissions, + UpdateDate = realGroup.UpdateDate + }; + } + set + { + //if old APIs are still using this lets first check if the user is part of the user group with the alias specified + if (Groups.Any(x => x.Alias == value.Alias)) + return; + + //the only other option we have here is to lookup the group (and we'll need to use singletons here :( ) + var found = ApplicationContext.Current.Services.UserService.GetUserGroupByAlias(value.Alias); + if (found == null) + throw new InvalidOperationException("No user group was found with the alias " + value.Alias + ", this API (IUser.UserType) is obsolete, use user groups instead"); + + //if it's found, all we can do is add it, we can't really replace them + AddGroup(found.ToReadOnlyGroup()); + } + } + + [Obsolete("This should not be used it exists for legacy reasons only, use user groups instead, it will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] + void IUser.RemoveAllowedSection(string sectionAlias) + { + //don't do anything if they aren't allowed it already + if (AllowedSections.Contains(sectionAlias) == false) + return; + + var groups = Groups.ToArray(); + //our only option here is to check if a custom group is created for this user, if so we can remove it from that group, otherwise we'll throw + //now we'll check if the user has a special 1:1 user group created for itself. This will occur if this method is used and also during an upgrade. + //this comes in the alias form of userName + 'Group' + var customUserGroup = groups.FirstOrDefault(x => x.Alias == (Username + "Group")); + if (customUserGroup != null) + { + //if the group isn't IUserGroup we'll need to look it up + var realGroup = customUserGroup as IUserGroup ?? ApplicationContext.Current.Services.UserService.GetUserGroupById(customUserGroup.Id); + realGroup.RemoveAllowedSection(sectionAlias); + //now we need to flag this for saving (hack!) + GroupsToSave.Add(realGroup); + } + else + { + throw new InvalidOperationException("Cannot remove the allowed section using this obsolete API. Modify the user's groups instead"); + } + + } + + [Obsolete("This should not be used it exists for legacy reasons only, use user groups instead, it will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] + void IUser.AddAllowedSection(string sectionAlias) + { + //don't do anything if they are allowed it already + if (AllowedSections.Contains(sectionAlias)) + return; + + //This is here for backwards compat only. + //First we'll check if the user is part of the 'admin' group. If so then we can ensure that the admin group has this section available to it. + //otherwise, the only thing we can do is create a custom user group for this user and add this section. + //We are checking for admin here because if the user is an admin and an allowed section is being added, then it's assumed it's to be added + //for the whole admin group (i.e. Forms installer does this for admins) + var groups = Groups.ToArray(); + var admin = groups.FirstOrDefault(x => x.Alias == Constants.Security.AdminGroupAlias); + if (admin != null) + { + //if the group isn't IUserGroup we'll need to look it up + var realGroup = admin as IUserGroup ?? ApplicationContext.Current.Services.UserService.GetUserGroupById(admin.Id); + realGroup.AddAllowedSection(sectionAlias); + //now we need to flag this for saving (hack!) + GroupsToSave.Add(realGroup); + } + + //now we'll check if the user has a special 1:1 user group created for itself. This will occur if this method is used and also during an upgrade. + //this comes in the alias form of userName + 'Group' + var customUserGroup = groups.FirstOrDefault(x => x.Alias == (Username + "Group")); + if (customUserGroup != null) + { + //if the group isn't IUserGroup we'll need to look it up + var realGroup = customUserGroup as IUserGroup ?? ApplicationContext.Current.Services.UserService.GetUserGroupById(customUserGroup.Id); + realGroup.AddAllowedSection(sectionAlias); + //now we need to flag this for saving (hack!) + GroupsToSave.Add(realGroup); + } + + //ok, so the user doesn't have a 1:1 group, we'll need to flag it for creation + var newUserGroup = new UserGroup + { + Alias = Username + "Group", + Name = "Group for " + Username + }; + newUserGroup.AddAllowedSection(sectionAlias); + GroupsToSave.Add(newUserGroup); + } + + /// + /// This used purely for hacking backwards compatibility into this class for < 7.7 compat + /// + [DoNotClone] + [IgnoreDataMember] + internal List GroupsToSave = new List(); + public IProfile ProfileData { get { return new WrappedUserProfile(this); } diff --git a/src/Umbraco.Core/Models/Membership/UserType.cs b/src/Umbraco.Core/Models/Membership/UserType.cs new file mode 100644 index 0000000000..13865efed9 --- /dev/null +++ b/src/Umbraco.Core/Models/Membership/UserType.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Reflection; +using System.Runtime.Serialization; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Strings; + +namespace Umbraco.Core.Models.Membership +{ + [Obsolete("This should not be used it exists for legacy reasons only, use user groups instead, it will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] + [Serializable] + [DataContract(IsReference = true)] + internal class UserType : Entity, IUserType + { + private string _alias; + private string _name; + private IEnumerable _permissions; + private static readonly Lazy Ps = new Lazy(); + 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); + } + + [DataMember] + public string Alias + { + get { return _alias; } + set + { + SetPropertyValueAndDetectChanges( + value.ToCleanString(CleanStringType.Alias | CleanStringType.UmbracoCase), + ref _alias, + Ps.Value.AliasSelector); + } + } + + [DataMember] + public string Name + { + get { return _name; } + set { SetPropertyValueAndDetectChanges(value, ref _name, Ps.Value.NameSelector); } + } + + /// + /// The set of default permissions for the user type + /// + /// + /// 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 { return _permissions; } + set + { + SetPropertyValueAndDetectChanges(value, ref _permissions, Ps.Value.PermissionsSelector, + //Custom comparer for enumerable + new DelegateEqualityComparer>( + (enum1, enum2) => enum1.UnsortedSequenceEqual(enum2), + enum1 => enum1.GetHashCode())); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 572453109d..7da5022d31 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -295,6 +295,18 @@ namespace Umbraco.Core.Services var repository = RepositoryFactory.CreateUserRepository(uow); repository.AddOrUpdate(entity); + + //Now we have to check for backwards compat hacks + var explicitUser = entity as User; + if (explicitUser != null && explicitUser.GroupsToSave.Count > 0) + { + var groupRepository = RepositoryFactory.CreateUserGroupRepository(uow); + foreach (var userGroup in explicitUser.GroupsToSave) + { + groupRepository.AddOrUpdate(userGroup); + } + } + try { // try to flush the unit of work @@ -340,17 +352,28 @@ namespace Umbraco.Core.Services } } var repository = RepositoryFactory.CreateUserRepository(uow); - foreach (var member in asArray) + var groupRepository = RepositoryFactory.CreateUserGroupRepository(uow); + foreach (var user in asArray) { - if (string.IsNullOrWhiteSpace(member.Username)) + if (string.IsNullOrWhiteSpace(user.Username)) { throw new ArgumentException("Cannot save user with empty username."); } - if (string.IsNullOrWhiteSpace(member.Name)) + if (string.IsNullOrWhiteSpace(user.Name)) { throw new ArgumentException("Cannot save user with empty name."); } - 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); + } + } } //commit the whole lot in one go uow.Commit(); diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 87206aa1c6..b79b89b963 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -351,10 +351,12 @@ + + diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs index a8846ff182..361d01ba75 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; @@ -24,6 +26,12 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "emailHash")] public string EmailHash { get; set; } + [Obsolete("This should not be used it exists for legacy reasons only, use user groups instead, it will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] + [ReadOnly(true)] + [DataMember(Name = "userType")] + public string UserType { get; set; } + /// /// Gets/sets the number of seconds for the user's auth ticket to expire /// diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index f5aac1d1fc..6dcd88eae4 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -315,7 +315,34 @@ namespace Umbraco.Web.Models.Mapping .ForMember( detail => detail.EmailHash, opt => opt.MapFrom(user => user.Email.ToLowerInvariant().Trim().GenerateHash())) - .ForMember(detail => detail.SecondsUntilTimeout, opt => opt.Ignore()); + .ForMember(detail => detail.SecondsUntilTimeout, opt => opt.Ignore()) + .AfterMap((user, detail) => + { + //we need to map the legacy UserType + //the best we can do here is to return the user's first user group as a IUserType object + //but we should attempt to return any group that is the built in ones first + var groups = user.Groups.ToArray(); + if (groups.Length == 0) + { + //In backwards compatibility land, a user type cannot be null! so we need to return a fake one. + detail.UserType = "temp"; + } + else + { + var builtIns = new[] { Constants.Security.AdminGroupAlias, "writer", "editor", "translator" }; + var foundBuiltIn = groups.FirstOrDefault(x => builtIns.Contains(x.Alias)); + if (foundBuiltIn != null) + { + detail.UserType = foundBuiltIn.Alias; + } + else + { + //otherwise return the first + detail.UserType = groups[0].Alias; + } + } + + }); config.CreateMap() .ForMember(detail => detail.UserId, opt => opt.MapFrom(profile => GetIntId(profile.Id)));