diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs index 087f5913e6..3ba5b4259a 100644 --- a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs @@ -16,6 +16,8 @@ namespace Umbraco.Core.Models.Identity public string[] AllowedApplications { get; set; } public string Culture { get; set; } + public string UserTypeAlias { get; set; } + /// /// Overridden to make the retrieval lazy /// diff --git a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs new file mode 100644 index 0000000000..8fa2703f39 --- /dev/null +++ b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs @@ -0,0 +1,34 @@ +using System; +using System.Linq; +using AutoMapper; + +using Umbraco.Core.Models.Mapping; +using Umbraco.Core.Models.Membership; + +namespace Umbraco.Core.Models.Identity +{ + public class IdentityModelMappings : MapperConfiguration + { + public override void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext) + { + config.CreateMap() + .ForMember(user => user.Email, expression => expression.MapFrom(user => user.Email)) + .ForMember(user => user.Id, expression => expression.MapFrom(user => user.Id)) + .ForMember(user => user.LockoutEnabled, expression => expression.MapFrom(user => user.IsLockedOut)) + .ForMember(user => user.LockoutEndDateUtc, expression => expression.UseValue(DateTime.MaxValue.ToUniversalTime())) + .ForMember(user => user.UserName, expression => expression.MapFrom(user => user.Username)) + .ForMember(user => user.PasswordHash, expression => expression.MapFrom(user => GetPasswordHash(user.RawPasswordValue))) + .ForMember(user => user.Culture, expression => expression.MapFrom(user => user.Language)) + .ForMember(user => user.Name, expression => expression.MapFrom(user => user.Name)) + .ForMember(user => user.StartMediaNode, expression => expression.MapFrom(user => user.StartMediaId)) + .ForMember(user => user.StartContentNode, expression => expression.MapFrom(user => user.StartContentId)) + .ForMember(user => user.UserTypeAlias, expression => expression.MapFrom(user => user.UserType.Alias)) + .ForMember(user => user.AllowedApplications, expression => expression.MapFrom(user => user.AllowedSections.ToArray())); + } + + private string GetPasswordHash(string storedPass) + { + return storedPass.StartsWith("___UIDEMPTYPWORD__") ? null : storedPass; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs b/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs index f9c77031e9..efed4ce37c 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs @@ -9,6 +9,13 @@ namespace Umbraco.Core.Models.Identity /// public class IdentityUserLogin : Entity, IIdentityUserLogin { + public IdentityUserLogin(string loginProvider, string providerKey, int userId) + { + LoginProvider = loginProvider; + ProviderKey = providerKey; + UserId = userId; + } + public IdentityUserLogin(int id, string loginProvider, string providerKey, int userId, DateTime createDate) { Id = id; diff --git a/src/Umbraco.Core/Models/Mapping/MapperConfiguration.cs b/src/Umbraco.Core/Models/Mapping/MapperConfiguration.cs index 52683231e2..046f4429b2 100644 --- a/src/Umbraco.Core/Models/Mapping/MapperConfiguration.cs +++ b/src/Umbraco.Core/Models/Mapping/MapperConfiguration.cs @@ -12,4 +12,6 @@ namespace Umbraco.Core.Models.Mapping { public abstract void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext); } + + } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Mappers/UserMapper.cs b/src/Umbraco.Core/Persistence/Mappers/UserMapper.cs index 9c51f53596..0ffd6cbfe1 100644 --- a/src/Umbraco.Core/Persistence/Mappers/UserMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/UserMapper.cs @@ -1,11 +1,41 @@ using System; using System.Collections.Concurrent; using System.Linq.Expressions; +using Umbraco.Core.Models.Identity; using Umbraco.Core.Models.Membership; using Umbraco.Core.Models.Rdbms; namespace Umbraco.Core.Persistence.Mappers { + [MapperFor(typeof(IIdentityUserLogin))] + [MapperFor(typeof(IdentityUserLogin))] + public sealed class ExternalLoginMapper : BaseMapper + { + private static readonly ConcurrentDictionary PropertyInfoCacheInstance = new ConcurrentDictionary(); + public ExternalLoginMapper() + { + BuildMap(); + } + + #region Overrides of BaseMapper + + internal override ConcurrentDictionary PropertyInfoCache + { + get { return PropertyInfoCacheInstance; } + } + + internal override void BuildMap() + { + CacheMap(src => src.Id, dto => dto.Id); + CacheMap(src => src.CreateDate, dto => dto.CreateDate); + CacheMap(src => src.LoginProvider, dto => dto.LoginProvider); + CacheMap(src => src.ProviderKey, dto => dto.ProviderKey); + CacheMap(src => src.UserId, dto => dto.UserId); + } + + #endregion + } + [MapperFor(typeof(IUser))] [MapperFor(typeof(User))] public sealed class UserMapper : BaseMapper diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index 427bc306da..8347bfe55c 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -3,8 +3,10 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Web.Security; +using AutoMapper; using Microsoft.AspNet.Identity; using Umbraco.Core.Models.Identity; +using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; namespace Umbraco.Core.Security @@ -13,22 +15,19 @@ namespace Umbraco.Core.Security { private readonly IUserService _userService; private readonly IExternalLoginService _externalLoginService; - private readonly MembershipProviderBase _usersMembershipProvider; public BackOfficeUserStore(IUserService userService, IExternalLoginService externalLoginService, MembershipProviderBase usersMembershipProvider) { _userService = userService; _externalLoginService = externalLoginService; - _usersMembershipProvider = usersMembershipProvider; if (userService == null) throw new ArgumentNullException("userService"); if (usersMembershipProvider == null) throw new ArgumentNullException("usersMembershipProvider"); if (externalLoginService == null) throw new ArgumentNullException("externalLoginService"); _userService = userService; - _usersMembershipProvider = usersMembershipProvider; _externalLoginService = externalLoginService; - if (_usersMembershipProvider.PasswordFormat != MembershipPasswordFormat.Hashed) + if (usersMembershipProvider.PasswordFormat != MembershipPasswordFormat.Hashed) { throw new InvalidOperationException("Cannot use ASP.Net Identity with UmbracoMembersUserStore when the password format is not Hashed"); } @@ -39,7 +38,6 @@ namespace Umbraco.Core.Security /// protected override void DisposeResources() { - throw new NotImplementedException(); } /// @@ -49,7 +47,43 @@ namespace Umbraco.Core.Security /// public Task CreateAsync(BackOfficeIdentityUser user) { - throw new NotImplementedException(); + if (user == null) throw new ArgumentNullException("user"); + + var userType = _userService.GetUserTypeByAlias( + user.UserTypeAlias.IsNullOrWhiteSpace() ? _userService.GetDefaultMemberType() : user.UserTypeAlias); + + var member = new User(userType) + { + DefaultToLiveEditing = false, + Email = user.Email, + Language = Configuration.GlobalSettings.DefaultUILanguage, + Name = user.Name, + Username = user.UserName, + StartContentId = -1, + StartMediaId = -1, + IsLockedOut = false, + IsApproved = true + }; + + UpdateMemberProperties(member, user); + + //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(member); + + //re-assign id + user.Id = member.Id; + + return Task.FromResult(0); } /// @@ -57,9 +91,30 @@ namespace Umbraco.Core.Security /// /// /// - public Task UpdateAsync(BackOfficeIdentityUser user) + public async Task UpdateAsync(BackOfficeIdentityUser user) { - throw new NotImplementedException(); + if (user == null) throw new ArgumentNullException("user"); + + var asInt = user.Id.TryConvertTo(); + if (asInt == false) + { + throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); + } + + var found = _userService.GetUserById(asInt.Result); + if (found != null) + { + if (UpdateMemberProperties(found, user)) + { + _userService.Save(found); + } + + if (user.LoginsChanged) + { + var logins = await GetLoginsAsync(user); + _externalLoginService.SaveUserLogins(found.Id, logins); + } + } } /// @@ -69,7 +124,22 @@ namespace Umbraco.Core.Security /// public Task DeleteAsync(BackOfficeIdentityUser user) { - throw new NotImplementedException(); + if (user == null) throw new ArgumentNullException("user"); + + var asInt = user.Id.TryConvertTo(); + if (asInt == false) + { + throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); + } + + var found = _userService.GetUserById(asInt.Result); + if (found != null) + { + _userService.Delete(found); + } + _externalLoginService.DeleteUserLogins(asInt.Result); + + return Task.FromResult(0); } /// @@ -79,7 +149,12 @@ namespace Umbraco.Core.Security /// public Task FindByIdAsync(int userId) { - throw new NotImplementedException(); + var user = _userService.GetUserById(userId); + if (user == null) + { + return null; + } + return Task.FromResult(AssignLoginsCallback(Mapper.Map(user))); } /// @@ -89,7 +164,15 @@ namespace Umbraco.Core.Security /// public Task FindByNameAsync(string userName) { - throw new NotImplementedException(); + var user = _userService.GetByUsername(userName); + if (user == null) + { + return null; + } + + var result = AssignLoginsCallback(Mapper.Map(user)); + + return Task.FromResult(result); } /// @@ -99,7 +182,12 @@ namespace Umbraco.Core.Security /// public Task SetPasswordHashAsync(BackOfficeIdentityUser user, string passwordHash) { - throw new NotImplementedException(); + if (user == null) throw new ArgumentNullException("user"); + if (passwordHash.IsNullOrWhiteSpace()) throw new ArgumentNullException("passwordHash"); + + user.PasswordHash = passwordHash; + + return Task.FromResult(0); } /// @@ -109,7 +197,9 @@ namespace Umbraco.Core.Security /// public Task GetPasswordHashAsync(BackOfficeIdentityUser user) { - throw new NotImplementedException(); + if (user == null) throw new ArgumentNullException("user"); + + return Task.FromResult(user.PasswordHash); } /// @@ -119,7 +209,9 @@ namespace Umbraco.Core.Security /// public Task HasPasswordAsync(BackOfficeIdentityUser user) { - throw new NotImplementedException(); + if (user == null) throw new ArgumentNullException("user"); + + return Task.FromResult(user.PasswordHash.IsNullOrWhiteSpace() == false); } /// @@ -129,7 +221,12 @@ namespace Umbraco.Core.Security /// public Task SetEmailAsync(BackOfficeIdentityUser user, string email) { - throw new NotImplementedException(); + if (user == null) throw new ArgumentNullException("user"); + if (email.IsNullOrWhiteSpace()) throw new ArgumentNullException("email"); + + user.Email = email; + + return Task.FromResult(0); } /// @@ -139,7 +236,9 @@ namespace Umbraco.Core.Security /// public Task GetEmailAsync(BackOfficeIdentityUser user) { - throw new NotImplementedException(); + if (user == null) throw new ArgumentNullException("user"); + + return Task.FromResult(user.Email); } /// @@ -169,7 +268,12 @@ namespace Umbraco.Core.Security /// public Task FindByEmailAsync(string email) { - throw new NotImplementedException(); + var user = _userService.GetByEmail(email); + var result = user == null + ? null + : Mapper.Map(user); + + return Task.FromResult(AssignLoginsCallback(result)); } /// @@ -179,7 +283,15 @@ namespace Umbraco.Core.Security /// public Task AddLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login) { - throw new NotImplementedException(); + if (user == null) throw new ArgumentNullException("user"); + if (login == null) throw new ArgumentNullException("login"); + + var logins = user.Logins; + var instance = new IdentityUserLogin(login.LoginProvider, login.ProviderKey, user.Id); + var userLogin = instance; + logins.Add(userLogin); + + return Task.FromResult(0); } /// @@ -189,7 +301,16 @@ namespace Umbraco.Core.Security /// public Task RemoveLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login) { - throw new NotImplementedException(); + if (user == null) throw new ArgumentNullException("user"); + if (login == null) throw new ArgumentNullException("login"); + + var provider = login.LoginProvider; + var key = login.ProviderKey; + var userLogin = user.Logins.SingleOrDefault((l => l.LoginProvider == provider && l.ProviderKey == key)); + if (userLogin != null) + user.Logins.Remove(userLogin); + + return Task.FromResult(0); } /// @@ -199,7 +320,9 @@ namespace Umbraco.Core.Security /// public Task> GetLoginsAsync(BackOfficeIdentityUser user) { - throw new NotImplementedException(); + if (user == null) throw new ArgumentNullException("user"); + return Task.FromResult((IList) + user.Logins.Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey)).ToList()); } /// @@ -213,21 +336,13 @@ namespace Umbraco.Core.Security if (result.Any()) { //return the first member that matches the result - var user = (from l in result + var output = (from l in result select _userService.GetUserById(l.Id) - into member - where member != null - select new BackOfficeIdentityUser - { - Email = member.Email, - Id = member.Id, - LockoutEnabled = member.IsLockedOut, - LockoutEndDateUtc = DateTime.MaxValue.ToUniversalTime(), - UserName = member.Username, - PasswordHash = GetPasswordHash(member.RawPasswordValue) - }).FirstOrDefault(); + into user + where user != null + select Mapper.Map(user)).FirstOrDefault(); - return Task.FromResult(AssignLoginsCallback(user)); + return Task.FromResult(AssignLoginsCallback(output)); } return Task.FromResult(null); @@ -243,9 +358,73 @@ namespace Umbraco.Core.Security return user; } - private string GetPasswordHash(string storedPass) + private bool UpdateMemberProperties(Models.Membership.IUser user, BackOfficeIdentityUser identityUser) { - return storedPass.StartsWith("___UIDEMPTYPWORD__") ? null : storedPass; + var anythingChanged = false; + //don't assign anything if nothing has changed as this will trigger + //the track changes of the model + if (user.Name != identityUser.Name && identityUser.Name.IsNullOrWhiteSpace() == false) + { + anythingChanged = true; + user.Name = identityUser.Name; + } + if (user.Email != identityUser.Email && identityUser.Email.IsNullOrWhiteSpace() == false) + { + anythingChanged = true; + user.Email = identityUser.Email; + } + if (user.FailedPasswordAttempts != identityUser.AccessFailedCount) + { + anythingChanged = true; + user.FailedPasswordAttempts = identityUser.AccessFailedCount; + } + if (user.IsLockedOut != identityUser.LockoutEnabled) + { + anythingChanged = true; + user.IsLockedOut = identityUser.LockoutEnabled; + } + if (user.Username != identityUser.UserName && identityUser.UserName.IsNullOrWhiteSpace() == false) + { + anythingChanged = true; + user.Username = identityUser.UserName; + } + if (user.RawPasswordValue != identityUser.PasswordHash && identityUser.PasswordHash.IsNullOrWhiteSpace() == false) + { + anythingChanged = true; + user.RawPasswordValue = identityUser.PasswordHash; + } + + if (user.Language != identityUser.Culture && identityUser.Culture.IsNullOrWhiteSpace() == false) + { + anythingChanged = true; + user.Language = identityUser.Culture; + } + if (user.StartMediaId != identityUser.StartMediaNode) + { + anythingChanged = true; + user.StartMediaId = identityUser.StartMediaNode; + } + if (user.StartContentId != identityUser.StartContentNode) + { + anythingChanged = true; + user.StartContentId = identityUser.StartContentNode; + } + if (user.AllowedSections.ContainsAll(identityUser.AllowedApplications) == false + || identityUser.AllowedApplications.ContainsAll(user.AllowedSections) == false) + { + anythingChanged = true; + foreach (var allowedSection in user.AllowedSections) + { + user.RemoveAllowedSection(allowedSection); + } + foreach (var allowedApplication in identityUser.AllowedApplications) + { + user.AddAllowedSection(allowedApplication); + } + } + + return anythingChanged; } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index f76cb2667c..bd32b08677 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -344,6 +344,7 @@ + diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index 27dfe80c6d..2267e30ca7 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -77,6 +77,8 @@ namespace Umbraco.Web.Security.Identity { if (app == null) throw new ArgumentNullException("app"); + //TODO: Figure out why this isn't working and is only working with the default one, must be a reference somewhere + //app.UseExternalSignInCookie("UmbracoExternalCookie"); app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);