From c95e560c545c952d570f07088e65206bcf240437 Mon Sep 17 00:00:00 2001 From: emmagarland Date: Mon, 23 Nov 2020 16:05:12 +0000 Subject: [PATCH 01/72] Initial creation of templates for the member identity user, store and manager. Very much in flux, needs tests and implementations. Based on BackOfficeIdentityUser and UmbracoIdentity. --- .../Members/UmbracoMembersIdentityUser.cs | 86 +++++++ .../Members/IUmbracoMembersUserManager.cs | 14 ++ .../Members/UmbracoMembersUserManager.cs | 47 ++++ .../Members/UmbracoMembersUserStore.cs | 236 ++++++++++++++++++ 4 files changed, 383 insertions(+) create mode 100644 src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs create mode 100644 src/Umbraco.Infrastructure/Members/IUmbracoMembersUserManager.cs create mode 100644 src/Umbraco.Infrastructure/Members/UmbracoMembersUserManager.cs create mode 100644 src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs diff --git a/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs b/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs new file mode 100644 index 0000000000..531460794a --- /dev/null +++ b/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Text; +using Umbraco.Core.Models.Entities; +using Umbraco.Core.Models.Identity; + +namespace Umbraco.Core.Members +{ + /// + /// An Umbraco member user type + /// + public class UmbracoMembersIdentityUser : IdentityUser, IdentityUserClaim>, IRememberBeingDirty + { + private readonly BeingDirty _beingDirty = new BeingDirty(); + + #region BeingDirty + public event PropertyChangedEventHandler PropertyChanged + { + add + { + _beingDirty.PropertyChanged += value; + } + remove + { + _beingDirty.PropertyChanged -= value; + } + } + + public void DisableChangeTracking() + { + throw new NotImplementedException(); + } + + public void EnableChangeTracking() + { + throw new NotImplementedException(); + } + + public IEnumerable GetDirtyProperties() + { + throw new NotImplementedException(); + } + + public IEnumerable GetWereDirtyProperties() + { + throw new NotImplementedException(); + } + + public bool IsDirty() + { + throw new NotImplementedException(); + } + + public bool IsPropertyDirty(string propName) + { + throw new NotImplementedException(); + } + + public void ResetDirtyProperties(bool rememberDirty) + { + throw new NotImplementedException(); + } + + public void ResetDirtyProperties() + { + throw new NotImplementedException(); + } + + public void ResetWereDirtyProperties() + { + throw new NotImplementedException(); + } + + public bool WasDirty() + { + throw new NotImplementedException(); + } + + public bool WasPropertyDirty(string propertyName) + { + throw new NotImplementedException(); + } + } + #endregion +} diff --git a/src/Umbraco.Infrastructure/Members/IUmbracoMembersUserManager.cs b/src/Umbraco.Infrastructure/Members/IUmbracoMembersUserManager.cs new file mode 100644 index 0000000000..646ce367e9 --- /dev/null +++ b/src/Umbraco.Infrastructure/Members/IUmbracoMembersUserManager.cs @@ -0,0 +1,14 @@ +using System; +using Umbraco.Core.Members; + +namespace Umbraco.Infrastructure.Members +{ + public interface IUmbracoMembersUserManager : IUmbracoMembersUserManager + { + } + + public interface IUmbracoMembersUserManager : IDisposable + where TUser : UmbracoMembersIdentityUser + { + } +} diff --git a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserManager.cs b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserManager.cs new file mode 100644 index 0000000000..d298039484 --- /dev/null +++ b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserManager.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using Umbraco.Core.Members; + +namespace Umbraco.Infrastructure.Members +{ + /// + /// A manager for the Umbraco members identity implementation + /// + public class UmbracoMembersUserManager : UmbracoMembersUserManager, IUmbracoMembersUserManager + { + public UmbracoMembersUserManager( + IUserStore store, + IOptions optionsAccessor, + IPasswordHasher passwordHasher, + IEnumerable> userValidators, + IEnumerable> passwordValidators, + ILookupNormalizer keyNormalizer, + IdentityErrorDescriber errors, + IServiceProvider services, + ILogger> logger) : + base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) + { + } + } + + public class UmbracoMembersUserManager : UserManager + where T : UmbracoMembersIdentityUser + { + public UmbracoMembersUserManager( + IUserStore store, + IOptions optionsAccessor, + IPasswordHasher passwordHasher, + IEnumerable> userValidators, + IEnumerable> passwordValidators, + ILookupNormalizer keyNormalizer, + IdentityErrorDescriber errors, + IServiceProvider services, + ILogger> logger) : + base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs new file mode 100644 index 0000000000..82df3a64ec --- /dev/null +++ b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs @@ -0,0 +1,236 @@ +using Microsoft.AspNetCore.Identity; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Umbraco.Core; +using Umbraco.Core.BackOffice; +using Umbraco.Core.Members; + +namespace Umbraco.Infrastructure.Members +{ + /// + /// A custom user store that uses Umbraco member data + /// + public class UmbracoMembersUserStore : DisposableObjectSlim, + IUserStore, + IUserPasswordStore, + IUserEmailStore, + IUserLoginStore, + IUserRoleStore, + IUserSecurityStampStore, + IUserLockoutStore, + IUserTwoFactorStore, + IUserSessionStore + { + public UmbracoMembersUserStore() + { + + } + + public Task AddLoginAsync(UmbracoMembersIdentityUser user, UserLoginInfo login, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task AddToRoleAsync(UmbracoMembersIdentityUser user, string roleName, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task CreateAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task DeleteAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task FindByIdAsync(string userId, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task GetAccessFailedCountAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task GetEmailAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task GetEmailConfirmedAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task GetLockoutEnabledAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task GetLockoutEndDateAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task> GetLoginsAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task GetNormalizedEmailAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task GetNormalizedUserNameAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task GetPasswordHashAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task> GetRolesAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task GetSecurityStampAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task GetTwoFactorEnabledAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task GetUserIdAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task GetUserNameAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task HasPasswordAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task IncrementAccessFailedCountAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task IsInRoleAsync(UmbracoMembersIdentityUser user, string roleName, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task RemoveFromRoleAsync(UmbracoMembersIdentityUser user, string roleName, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task RemoveLoginAsync(UmbracoMembersIdentityUser user, string loginProvider, string providerKey, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task ResetAccessFailedCountAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task SetEmailAsync(UmbracoMembersIdentityUser user, string email, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task SetEmailConfirmedAsync(UmbracoMembersIdentityUser user, bool confirmed, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task SetLockoutEnabledAsync(UmbracoMembersIdentityUser user, bool enabled, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task SetLockoutEndDateAsync(UmbracoMembersIdentityUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task SetNormalizedEmailAsync(UmbracoMembersIdentityUser user, string normalizedEmail, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task SetNormalizedUserNameAsync(UmbracoMembersIdentityUser user, string normalizedName, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task SetPasswordHashAsync(UmbracoMembersIdentityUser user, string passwordHash, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task SetSecurityStampAsync(UmbracoMembersIdentityUser user, string stamp, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task SetTwoFactorEnabledAsync(UmbracoMembersIdentityUser user, bool enabled, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task SetUserNameAsync(UmbracoMembersIdentityUser user, string userName, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task UpdateAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task ValidateSessionIdAsync(string userId, string sessionId) + { + throw new NotImplementedException(); + } + } +} From 8572d7bbd9003b56290dd58ad7c18e6a6ff808e2 Mon Sep 17 00:00:00 2001 From: emmagarland Date: Sun, 29 Nov 2020 13:56:58 +0000 Subject: [PATCH 02/72] Updated member type, and added start of unit tests for new store. Removed all unused properties for now to implement later. --- src/Umbraco.Core/Constants-Security.cs | 3 + .../Members/UmbracoMembersIdentityUser.cs | 92 +---- .../Members/UmbracoMembersUserStore.cs | 386 ++++++++++-------- .../UmbracoMemberIdentityUserStoreTests.cs | 75 ++++ 4 files changed, 323 insertions(+), 233 deletions(-) create mode 100644 src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserStoreTests.cs diff --git a/src/Umbraco.Core/Constants-Security.cs b/src/Umbraco.Core/Constants-Security.cs index 24b8b20731..42bace2938 100644 --- a/src/Umbraco.Core/Constants-Security.cs +++ b/src/Umbraco.Core/Constants-Security.cs @@ -40,6 +40,9 @@ public const string EmptyPasswordPrefix = "___UIDEMPTYPWORD__"; public const string ForceReAuthFlag = "umbraco-force-auth"; + public const string DefaultMemberTypeAlias = "Member"; + + /// /// The prefix used for external identity providers for their authentication type /// diff --git a/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs b/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs index 531460794a..c561767024 100644 --- a/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs +++ b/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Text; using Umbraco.Core.Models.Entities; using Umbraco.Core.Models.Identity; @@ -10,77 +7,28 @@ namespace Umbraco.Core.Members /// /// An Umbraco member user type /// - public class UmbracoMembersIdentityUser : IdentityUser, IdentityUserClaim>, IRememberBeingDirty + public class UmbracoMembersIdentityUser + //: IRememberBeingDirty + //TODO: use of identity classes + //: IdentityUser, IdentityUserClaim>, { - private readonly BeingDirty _beingDirty = new BeingDirty(); + public int Id; + public string Name; + public string Email; + public string UserName; + public string MemberTypeAlias; + public bool IsLockedOut; - #region BeingDirty - public event PropertyChangedEventHandler PropertyChanged - { - add - { - _beingDirty.PropertyChanged += value; - } - remove - { - _beingDirty.PropertyChanged -= value; - } - } + string Comment; + bool IsApproved; + DateTime LastLockoutDate; + DateTime CreationDate; + DateTime LastLoginDate; + DateTime LastActivityDate; + DateTime LastPasswordChangedDate; - public void DisableChangeTracking() - { - throw new NotImplementedException(); - } - - public void EnableChangeTracking() - { - throw new NotImplementedException(); - } - - public IEnumerable GetDirtyProperties() - { - throw new NotImplementedException(); - } - - public IEnumerable GetWereDirtyProperties() - { - throw new NotImplementedException(); - } - - public bool IsDirty() - { - throw new NotImplementedException(); - } - - public bool IsPropertyDirty(string propName) - { - throw new NotImplementedException(); - } - - public void ResetDirtyProperties(bool rememberDirty) - { - throw new NotImplementedException(); - } - - public void ResetDirtyProperties() - { - throw new NotImplementedException(); - } - - public void ResetWereDirtyProperties() - { - throw new NotImplementedException(); - } - - public bool WasDirty() - { - throw new NotImplementedException(); - } - - public bool WasPropertyDirty(string propertyName) - { - throw new NotImplementedException(); - } + //TODO: needed? + //public bool LoginsChanged; + //public bool RolesChanged; } - #endregion } diff --git a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs index 82df3a64ec..10c85a9def 100644 --- a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs +++ b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs @@ -1,11 +1,17 @@ using Microsoft.AspNetCore.Identity; using System; using System.Collections.Generic; +using System.Data; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Umbraco.Core; using Umbraco.Core.BackOffice; using Umbraco.Core.Members; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Services; namespace Umbraco.Infrastructure.Members { @@ -13,121 +19,253 @@ namespace Umbraco.Infrastructure.Members /// A custom user store that uses Umbraco member data /// public class UmbracoMembersUserStore : DisposableObjectSlim, - IUserStore, - IUserPasswordStore, - IUserEmailStore, - IUserLoginStore, - IUserRoleStore, - IUserSecurityStampStore, - IUserLockoutStore, - IUserTwoFactorStore, - IUserSessionStore + IUserStore + //IUserPasswordStore + //IUserEmailStore + //IUserLoginStore + //IUserRoleStore, + //IUserSecurityStampStore + //IUserLockoutStore + //IUserTwoFactorStore + //IUserSessionStore { - public UmbracoMembersUserStore() - { + private bool _disposed = false; + private IMemberService _memberService; + public UmbracoMembersUserStore(IMemberService memberService) + { + _memberService = memberService; } - public Task AddLoginAsync(UmbracoMembersIdentityUser user, UserLoginInfo login, CancellationToken cancellationToken) + + public Task CreateAsync(UmbracoMembersIdentityUser memberUser, CancellationToken cancellationToken) { - throw new NotImplementedException(); + //TODO: cancellationToken.ThrowIfCancellationRequested(); + //TODO: ThrowIfDisposed(); + if (memberUser == null) throw new ArgumentNullException(nameof(memberUser)); + + + // [Comments from Identity package and BackOfficeUser] + // 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(memberUser, Guid.NewGuid().ToString("N")); + + //create member + //TODO: are we keeping this method, e.g. the Member Service? + IMember memberEntity = _memberService.CreateMember( + memberUser.UserName, + memberUser.Email, + memberUser.Name.IsNullOrWhiteSpace() ? memberUser.UserName : memberUser.Name, + memberUser.MemberTypeAlias.IsNullOrWhiteSpace() ? + Constants.Security.DefaultMemberTypeAlias : memberUser.MemberTypeAlias); + + + UpdateMemberProperties(memberEntity, memberUser); + + _memberService.Save(memberEntity); + + //re-assign id + memberUser.Id = memberEntity.Id; + + // TODO: do we need this? + // TODO: [from backofficeuser] we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. + //bool isLoginsPropertyDirty = member.IsPropertyDirty(nameof(UmbracoMembersIdentityUser.Logins)); + + //if (isLoginsPropertyDirty) + //{ + // _externalLoginService.Save( + // user.Id, + // user.Logins.Select(x => new ExternalLogin( + // x.LoginProvider, + // x.ProviderKey, + // x.UserData))); + //} + + if (!memberEntity.HasIdentity) throw new DataException("Could not create the user, check logs for details"); + + return Task.FromResult(IdentityResult.Success); + + //TODO: confirm + //if (memberUser.LoginsChanged) + //{ + // var logins = await GetLoginsAsync(memberUser); + // _externalLoginStore.SaveUserLogins(member.Id, logins); + //} + + //TODO: confirm + //if (memberUser.RolesChanged) + //{ + //IMembershipRoleService memberRoleService = _memberService; + + //var persistedRoles = memberRoleService.GetAllRoles(member.Id).ToArray(); + //var userRoles = memberUser.Roles.Select(x => x.RoleName).ToArray(); + + //var keep = persistedRoles.Intersect(userRoles).ToArray(); + //var remove = persistedRoles.Except(keep).ToArray(); + //var add = userRoles.Except(persistedRoles).ToArray(); + + //memberRoleService.DissociateRoles(new[] { member.Id }, remove); + //memberRoleService.AssignRoles(new[] { member.Id }, add); + //} } - public Task AddToRoleAsync(UmbracoMembersIdentityUser user, string roleName, CancellationToken cancellationToken) + + private bool UpdateMemberProperties(IMember member, UmbracoMembersIdentityUser memberIdentityUser) { - throw new NotImplementedException(); + //[Comments as per BackOfficeUserStore & identity package] + var anythingChanged = false; + //don't assign anything if nothing has changed as this will trigger the track changes of the model + + //if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.LastLoginDateUtc)) + // || (member.LastLoginDate != default(DateTime) && identityUser.LastLoginDateUtc.HasValue == false) + // || identityUser.LastLoginDateUtc.HasValue && member.LastLoginDate.ToUniversalTime() != identityUser.LastLoginDateUtc.Value) + //{ + // anythingChanged = true; + // //if the LastLoginDate is being set to MinValue, don't convert it ToLocalTime + // var dt = identityUser.LastLoginDateUtc == DateTime.MinValue ? DateTime.MinValue : identityUser.LastLoginDateUtc.Value.ToLocalTime(); + // member.LastLoginDate = dt; + //} + + //if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.LastPasswordChangeDateUtc)) + // || (member.LastPasswordChangeDate != default(DateTime) && identityUser.LastPasswordChangeDateUtc.HasValue == false) + // || identityUser.LastPasswordChangeDateUtc.HasValue && member.LastPasswordChangeDate.ToUniversalTime() != identityUser.LastPasswordChangeDateUtc.Value) + //{ + // anythingChanged = true; + // member.LastPasswordChangeDate = identityUser.LastPasswordChangeDateUtc.Value.ToLocalTime(); + //} + + //if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.EmailConfirmed)) + // || (member.EmailConfirmedDate.HasValue && member.EmailConfirmedDate.Value != default(DateTime) && identityUser.EmailConfirmed == false) + // || ((member.EmailConfirmedDate.HasValue == false || member.EmailConfirmedDate.Value == default(DateTime)) && identityUser.EmailConfirmed)) + //{ + // anythingChanged = true; + // member.EmailConfirmedDate = identityUser.EmailConfirmed ? (DateTime?)DateTime.Now : null; + //} + + if ( + //memberIdentityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Name)) && + member.Name != memberIdentityUser.Name && memberIdentityUser.Name.IsNullOrWhiteSpace() == false) + { + anythingChanged = true; + member.Name = memberIdentityUser.Name; + } + if ( + //memberIdentityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Email)) && + member.Email != memberIdentityUser.Email && memberIdentityUser.Email.IsNullOrWhiteSpace() == false) + { + anythingChanged = true; + member.Email = memberIdentityUser.Email; + } + + //TODO: AccessFailedCount + //if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.AccessFailedCount)) + // && member.FailedPasswordAttempts != identityUser.AccessFailedCount) + //{ + // anythingChanged = true; + // member.FailedPasswordAttempts = identityUser.AccessFailedCount; + //} + + if (member.IsLockedOut != memberIdentityUser.IsLockedOut) + { + anythingChanged = true; + member.IsLockedOut = memberIdentityUser.IsLockedOut; + + if (member.IsLockedOut) + { + //need to set the last lockout date + member.LastLockoutDate = DateTime.Now; + } + } + if ( + //memberIdentityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.UserName)) && + member.Username != memberIdentityUser.UserName && memberIdentityUser.UserName.IsNullOrWhiteSpace() == false) + { + anythingChanged = true; + member.Username = memberIdentityUser.UserName; + } + + //TODO: PasswordHash and PasswordConfig + //if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.PasswordHash)) + // && member.RawPasswordValue != identityUser.PasswordHash && identityUser.PasswordHash.IsNullOrWhiteSpace() == false) + //{ + // anythingChanged = true; + // member.RawPasswordValue = identityUser.PasswordHash; + // member.PasswordConfiguration = identityUser.PasswordConfig; + //} + + //TODO: SecurityStamp + //if (member.SecurityStamp != identityUser.SecurityStamp) + //{ + // anythingChanged = true; + // member.SecurityStamp = identityUser.SecurityStamp; + //} + + // TODO: Roles + // [Comment] Same comment as per BackOfficeUserStore: Fix this for Groups too + //if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Roles)) || identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Groups))) + //{ + // var userGroupAliases = member.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) + // { + // anythingChanged = true; + + // //clear out the current groups (need to ToArray since we are modifying the iterator) + // member.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) + // { + // member.AddGroup(group); + // } + + // //re-assign + // identityUser.Groups = groups; + // } + //} + + //TODO: reset all changes + //memberIdentityUser.ResetDirtyProperties(false); + + return anythingChanged; } - public Task CreateAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } public Task DeleteAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) { throw new NotImplementedException(); } - public Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - public Task FindByIdAsync(string userId, CancellationToken cancellationToken) { throw new NotImplementedException(); } - public Task FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - public Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) { throw new NotImplementedException(); } - public Task GetAccessFailedCountAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task GetEmailAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task GetEmailConfirmedAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task GetLockoutEnabledAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task GetLockoutEndDateAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task> GetLoginsAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task GetNormalizedEmailAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - public Task GetNormalizedUserNameAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) { throw new NotImplementedException(); } - public Task GetPasswordHashAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task> GetRolesAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task GetSecurityStampAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task GetTwoFactorEnabledAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - public Task GetUserIdAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) { throw new NotImplementedException(); @@ -138,86 +276,11 @@ namespace Umbraco.Infrastructure.Members throw new NotImplementedException(); } - public Task> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task HasPasswordAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task IncrementAccessFailedCountAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task IsInRoleAsync(UmbracoMembersIdentityUser user, string roleName, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task RemoveFromRoleAsync(UmbracoMembersIdentityUser user, string roleName, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task RemoveLoginAsync(UmbracoMembersIdentityUser user, string loginProvider, string providerKey, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task ResetAccessFailedCountAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task SetEmailAsync(UmbracoMembersIdentityUser user, string email, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task SetEmailConfirmedAsync(UmbracoMembersIdentityUser user, bool confirmed, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task SetLockoutEnabledAsync(UmbracoMembersIdentityUser user, bool enabled, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task SetLockoutEndDateAsync(UmbracoMembersIdentityUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task SetNormalizedEmailAsync(UmbracoMembersIdentityUser user, string normalizedEmail, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - public Task SetNormalizedUserNameAsync(UmbracoMembersIdentityUser user, string normalizedName, CancellationToken cancellationToken) { throw new NotImplementedException(); } - public Task SetPasswordHashAsync(UmbracoMembersIdentityUser user, string passwordHash, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task SetSecurityStampAsync(UmbracoMembersIdentityUser user, string stamp, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task SetTwoFactorEnabledAsync(UmbracoMembersIdentityUser user, bool enabled, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - public Task SetUserNameAsync(UmbracoMembersIdentityUser user, string userName, CancellationToken cancellationToken) { throw new NotImplementedException(); @@ -228,9 +291,10 @@ namespace Umbraco.Infrastructure.Members throw new NotImplementedException(); } - public Task ValidateSessionIdAsync(string userId, string sessionId) + private void ThrowIfDisposed() { - throw new NotImplementedException(); + if (_disposed) + throw new ObjectDisposedException(GetType().Name); } } } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserStoreTests.cs new file mode 100644 index 0000000000..335c5ef213 --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserStoreTests.cs @@ -0,0 +1,75 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Moq; +using NUnit.Framework; +using Umbraco.Core.Members; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Infrastructure.Members; +using Umbraco.Tests.UnitTests.Umbraco.Core.ShortStringHelper; + +namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Members +{ + [TestFixture] + public class UmbracoMemberIdentityUserStoreTests + { + private Mock _mockMemberService; + + public UmbracoMembersUserStore CreateSut() + { + _mockMemberService = new Mock(); + return new UmbracoMembersUserStore(_mockMemberService.Object); + } + + [Test] + public void GivenICreateUser_AndTheUserIsNull_ThenIShouldGetAFailedResultAsync() + { + //arrange + UmbracoMembersUserStore sut = CreateSut(); + CancellationToken fakeCancellationToken = new CancellationToken(){}; + + //act + Action actual = () => sut.CreateAsync(null, fakeCancellationToken); + + //assert + Assert.That(actual, Throws.ArgumentNullException); + } + + + [Test] + public async Task GivenICreateUser_AndTheUserIsPopulatedCorrectly_ThenIShouldGetASuccessResult() + { + //arrange + UmbracoMembersUserStore sut = CreateSut(); + UmbracoMembersIdentityUser fakeUser = new UmbracoMembersIdentityUser() + { + + }; + + CancellationToken fakeCancellationToken = new CancellationToken() { }; + + IMemberType fakeMemberType = new MemberType(new MockShortStringHelper(), 77); + + var mockMember = Mock.Of(m => + m.Name == "fakeName" && + m.Email == "fakeemail@umbraco.com" && + m.Username == "fakeUsername" && + m.RawPasswordValue == "fakePassword" && + m.ContentTypeAlias == fakeMemberType.Alias && + m.HasIdentity == true); + + bool raiseEvents = false; + + _mockMemberService.Setup(x => x.CreateMember(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(mockMember); + _mockMemberService.Setup(x => x.Save(mockMember, raiseEvents)); + + //act + IdentityResult identityResult = await sut.CreateAsync(fakeUser, fakeCancellationToken); + + //assert + Assert.IsTrue(identityResult.Succeeded); + } + } +} From e7a21bcc1899f73158a97f20af62e0b556f310e1 Mon Sep 17 00:00:00 2001 From: emmagarland Date: Sun, 29 Nov 2020 13:59:08 +0000 Subject: [PATCH 03/72] Check for no errors --- .../Members/UmbracoMemberIdentityUserStoreTests.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserStoreTests.cs index 335c5ef213..830ff4f14d 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserStoreTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserStoreTests.cs @@ -1,6 +1,8 @@ using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using Castle.Core.Internal; using Microsoft.AspNetCore.Identity; using Moq; using NUnit.Framework; @@ -70,6 +72,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Members //assert Assert.IsTrue(identityResult.Succeeded); + Assert.IsTrue(!identityResult.Errors.Any()); } } } From 5b4b948543be137adebdeccd737d4a362d15908b Mon Sep 17 00:00:00 2001 From: emmagarland Date: Mon, 30 Nov 2020 00:45:38 +0000 Subject: [PATCH 04/72] Added more layers to have a vertical slice through Umbraco for CreateMember via backoffice. Still lots to implement and test, not complete and needs early review. --- .../BackOffice/IdentityMapDefinition.cs | 31 +++ .../IUmbracoMembersUserPasswordChecker.cs | 19 ++ .../Members/UmbracoMembersIdentityUser.cs | 52 ++++- ...UmbracoMembersUserPasswordCheckerResult.cs | 12 + .../Members/IUmbracoMembersUserManager.cs | 44 +++- .../Members/UmbracoMembersIdentityBuilder.cs | 40 ++++ .../Members/UmbracoMembersIdentityOptions.cs | 11 + .../Members/UmbracoMembersUserManager.cs | 167 +++++++++++++- .../Members/UmbracoMembersUserStore.cs | 139 +++++++++--- .../UmbracoTestServerTestBase.cs | 1 + ...kOfficeServiceCollectionExtensionsTests.cs | 2 +- ...MembersServiceCollectionExtensionsTests.cs | 43 ++++ .../UmbracoMemberIdentityUserManagerTests.cs | 153 +++++++++++++ .../UmbracoMemberIdentityUserStoreTests.cs | 38 +++- .../Controllers/MemberController.cs | 214 ++++++++++++------ .../Extensions/IdentityBuilderExtensions.cs | 3 +- .../Extensions/UmbracoBuilderExtensions.cs | 4 + .../UmbracoMemberIdentityBuilderExtensions.cs | 23 ++ ...oMembersUserServiceCollectionExtensions.cs | 35 +++ .../Trees/MemberTreeController.cs | 1 + 20 files changed, 905 insertions(+), 127 deletions(-) create mode 100644 src/Umbraco.Core/Members/IUmbracoMembersUserPasswordChecker.cs create mode 100644 src/Umbraco.Core/Members/UmbracoMembersUserPasswordCheckerResult.cs create mode 100644 src/Umbraco.Infrastructure/Members/UmbracoMembersIdentityBuilder.cs create mode 100644 src/Umbraco.Infrastructure/Members/UmbracoMembersIdentityOptions.cs create mode 100644 src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoMembersServiceCollectionExtensionsTests.cs create mode 100644 src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserManagerTests.cs create mode 100644 src/Umbraco.Web.BackOffice/Extensions/UmbracoMemberIdentityBuilderExtensions.cs create mode 100644 src/Umbraco.Web.BackOffice/Extensions/UmbracoMembersUserServiceCollectionExtensions.cs diff --git a/src/Umbraco.Core/BackOffice/IdentityMapDefinition.cs b/src/Umbraco.Core/BackOffice/IdentityMapDefinition.cs index 61fdf82d19..caa8f41ea5 100644 --- a/src/Umbraco.Core/BackOffice/IdentityMapDefinition.cs +++ b/src/Umbraco.Core/BackOffice/IdentityMapDefinition.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Options; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Mapping; +using Umbraco.Core.Members; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; @@ -37,6 +38,20 @@ namespace Umbraco.Core.BackOffice target.ResetDirtyProperties(true); target.EnableChangeTracking(); }); + + mapper.Define( + (source, context) => + { + var target = new UmbracoMembersIdentityUser(); + //target.DisableChangeTracking(); + return target; + }, + (source, target, context) => + { + Map(source, target); + //target.ResetDirtyProperties(true); + //target.EnableChangeTracking(); + }); } // Umbraco.Code.MapAll -Id -Groups -LockoutEnabled -PhoneNumber -PhoneNumberConfirmed -TwoFactorEnabled @@ -76,6 +91,22 @@ namespace Umbraco.Core.BackOffice //target.Roles =; } + private void Map(IMember source, UmbracoMembersIdentityUser target) + { + target.Email = source.Email; + target.UserName = source.Username; + target.LastPasswordChangeDateUtc = source.LastPasswordChangeDate.ToUniversalTime(); + //target.LastLoginDateUtc = source.LastLoginDate.ToUniversalTime(); + //target.EmailConfirmed = source.EmailConfirmedDate.HasValue; + target.Name = source.Name; + //target.AccessFailedCount = source.FailedPasswordAttempts; + target.PasswordHash = GetPasswordHash(source.RawPasswordValue); + target.PasswordConfig = source.PasswordConfiguration; + target.IsApproved = source.IsApproved; + //target.SecurityStamp = source.SecurityStamp; + //target.LockoutEndDateUtc = source.IsLockedOut ? DateTime.MaxValue.ToUniversalTime() : (DateTime?)null; + } + private static string GetPasswordHash(string storedPass) { return storedPass.StartsWith(Constants.Security.EmptyPasswordPrefix) ? null : storedPass; diff --git a/src/Umbraco.Core/Members/IUmbracoMembersUserPasswordChecker.cs b/src/Umbraco.Core/Members/IUmbracoMembersUserPasswordChecker.cs new file mode 100644 index 0000000000..b361ca3121 --- /dev/null +++ b/src/Umbraco.Core/Members/IUmbracoMembersUserPasswordChecker.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; + +namespace Umbraco.Core.Members +{ + /// + /// Used by the UmbracoMembersUserManager to check the username/password which allows for developers to more easily + /// set the logic for this procedure. + /// + public interface IUmbracoMembersUserPasswordChecker + { + /// + /// Checks a password for a member + /// + /// + /// + /// + Task CheckPasswordAsync(UmbracoMembersIdentityUser member, string password); + } +} diff --git a/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs b/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs index c561767024..f3d5661926 100644 --- a/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs +++ b/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs @@ -12,23 +12,59 @@ namespace Umbraco.Core.Members //TODO: use of identity classes //: IdentityUser, IdentityUserClaim>, { - public int Id; - public string Name; - public string Email; - public string UserName; - public string MemberTypeAlias; - public bool IsLockedOut; + private bool _hasIdentity; + + public int Id { get; set; } + public string Name { get; set; } + public string Email { get; set; } + public string UserName { get; set; } + public string MemberTypeAlias { get; set; } + public bool IsLockedOut { get; set; } + + public string RawPasswordValue { get; set; } + public DateTime LastPasswordChangeDateUtc { get; set; } + + /// + /// Returns true if an Id has been set on this object + /// This will be false if the object is new and not persisted to the database + /// + public bool HasIdentity => _hasIdentity; + + //TODO: track + public string PasswordHash { get; set; } + + //TODO: config + public string PasswordConfig { get; set; } string Comment; - bool IsApproved; + internal bool IsApproved; DateTime LastLockoutDate; DateTime CreationDate; DateTime LastLoginDate; DateTime LastActivityDate; - DateTime LastPasswordChangedDate; //TODO: needed? //public bool LoginsChanged; //public bool RolesChanged; + + + public static UmbracoMembersIdentityUser CreateNew(string username, string email, string name = null) + { + if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username)); + + //no groups/roles yet + var member = new UmbracoMembersIdentityUser + { + UserName = username, + Email = email, + Name = name, + Id = 0, //TODO + _hasIdentity = false + }; + + //TODO: do we use this? + //member.EnableChangeTracking(); + return member; + } } } diff --git a/src/Umbraco.Core/Members/UmbracoMembersUserPasswordCheckerResult.cs b/src/Umbraco.Core/Members/UmbracoMembersUserPasswordCheckerResult.cs new file mode 100644 index 0000000000..8432b7b0bf --- /dev/null +++ b/src/Umbraco.Core/Members/UmbracoMembersUserPasswordCheckerResult.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Core.Members +{ + /// + /// The result returned from the IUmbracoMembersUserPasswordChecker + /// + public enum UmbracoMembersUserPasswordCheckerResult + { + ValidCredentials, + InvalidCredentials, + FallbackToDefaultChecker + } +} diff --git a/src/Umbraco.Infrastructure/Members/IUmbracoMembersUserManager.cs b/src/Umbraco.Infrastructure/Members/IUmbracoMembersUserManager.cs index 646ce367e9..81b11f5141 100644 --- a/src/Umbraco.Infrastructure/Members/IUmbracoMembersUserManager.cs +++ b/src/Umbraco.Infrastructure/Members/IUmbracoMembersUserManager.cs @@ -1,4 +1,6 @@ using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; using Umbraco.Core.Members; namespace Umbraco.Infrastructure.Members @@ -7,8 +9,46 @@ namespace Umbraco.Infrastructure.Members { } - public interface IUmbracoMembersUserManager : IDisposable - where TUser : UmbracoMembersIdentityUser + public interface IUmbracoMembersUserManager : IDisposable where TUser : UmbracoMembersIdentityUser { + /// + /// Creates the specified in the backing store with no password, + /// as an asynchronous operation. + /// + /// The member to create. + /// + /// The that represents the asynchronous operation, containing the + /// of the operation. + /// + Task CreateAsync(TUser memberUser); + + /// + /// Helper method to generate a password for a user based on the current password validator + /// + /// + string GeneratePassword(); + + /// + /// Adds the to the specified only if the user + /// does not already have a password. + /// + /// The member whose password should be set. + /// The password to set. + /// + /// The that represents the asynchronous operation, containing the + /// of the operation. + /// + Task AddPasswordAsync(TUser memberUser, string password); + + /// + /// Returns a flag indicating whether the given is valid for the + /// specified . + /// + /// The user whose password should be validated. + /// The password to validate + /// The that represents the asynchronous operation, containing true if + /// the specified matches the one store for the , + /// otherwise false. + Task CheckPasswordAsync(TUser memberUser, string password); } } diff --git a/src/Umbraco.Infrastructure/Members/UmbracoMembersIdentityBuilder.cs b/src/Umbraco.Infrastructure/Members/UmbracoMembersIdentityBuilder.cs new file mode 100644 index 0000000000..e195dc925c --- /dev/null +++ b/src/Umbraco.Infrastructure/Members/UmbracoMembersIdentityBuilder.cs @@ -0,0 +1,40 @@ +using System; +using System.Reflection; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Core.Members; + +namespace Umbraco.Infrastructure.Members +{ + public class UmbracoMembersIdentityBuilder : IdentityBuilder + { + + public UmbracoMembersIdentityBuilder(IServiceCollection services) : base(typeof(UmbracoMembersIdentityUser), services) + { + } + + public UmbracoMembersIdentityBuilder(Type role, IServiceCollection services) : base(typeof(UmbracoMembersIdentityUser), role, services) + { + } + + /// + /// Adds a token provider for the . + /// + /// The name of the provider to add. + /// The type of the to add. + /// The current instance. + public override IdentityBuilder AddTokenProvider(string providerName, Type provider) + { + if (!typeof(IUserTwoFactorTokenProvider<>).MakeGenericType(UserType).GetTypeInfo().IsAssignableFrom(provider.GetTypeInfo())) + { + throw new InvalidOperationException($"Invalid Type for TokenProvider: {provider.FullName}"); + } + Services.Configure(options => + { + options.Tokens.ProviderMap[providerName] = new TokenProviderDescriptor(provider); + }); + Services.AddTransient(provider); + return this; + } + } +} diff --git a/src/Umbraco.Infrastructure/Members/UmbracoMembersIdentityOptions.cs b/src/Umbraco.Infrastructure/Members/UmbracoMembersIdentityOptions.cs new file mode 100644 index 0000000000..e72b2e3aba --- /dev/null +++ b/src/Umbraco.Infrastructure/Members/UmbracoMembersIdentityOptions.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Identity; + +namespace Umbraco.Infrastructure.Members +{ + /// + /// Identity options specifically for the Umbraco members identity implementation + /// + public class UmbracoMembersIdentityOptions : IdentityOptions + { + } +} diff --git a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserManager.cs b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserManager.cs index d298039484..717d054120 100644 --- a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserManager.cs +++ b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserManager.cs @@ -3,7 +3,16 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Umbraco.Core.Configuration; using Umbraco.Core.Members; +using Umbraco.Core.Security; +using System.Threading; +using Umbraco.Core; +using Umbraco.Core.Configuration.Models; +using Umbraco.Web.Models.ContentEditing; + namespace Umbraco.Infrastructure.Members { @@ -14,15 +23,16 @@ namespace Umbraco.Infrastructure.Members { public UmbracoMembersUserManager( IUserStore store, - IOptions optionsAccessor, + IOptions optionsAccessor, IPasswordHasher passwordHasher, IEnumerable> userValidators, IEnumerable> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, - ILogger> logger) : - base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) + ILogger> logger, + IOptions passwordConfiguration) : + base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger, passwordConfiguration) { } } @@ -30,6 +40,10 @@ namespace Umbraco.Infrastructure.Members public class UmbracoMembersUserManager : UserManager where T : UmbracoMembersIdentityUser { + public IPasswordConfiguration PasswordConfiguration { get; protected set; } + + private PasswordGenerator _passwordGenerator; + public UmbracoMembersUserManager( IUserStore store, IOptions optionsAccessor, @@ -39,9 +53,154 @@ namespace Umbraco.Infrastructure.Members ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, - ILogger> logger) : + ILogger> logger, + IOptions passwordConfiguration) : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) { + PasswordConfiguration = passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration)); + } + + /// + /// Replace the underlying options property with our own strongly typed version + /// + public new UmbracoMembersIdentityOptions Options + { + get => (UmbracoMembersIdentityOptions)base.Options; + set => base.Options = value; + } + + /// + /// Gets/sets the default Umbraco member user password checker + /// + public IUmbracoMembersUserPasswordChecker UmbracoMembersUserPasswordChecker { get; set; } + + /// + /// [TODO: from BackOfficeUserManager duplicated, could be shared] + /// 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 UpdatePasswordHash(T memberUser, string newPassword, bool validatePassword) + { + memberUser.LastPasswordChangeDateUtc = DateTime.UtcNow; + + if (validatePassword) + { + IdentityResult validate = await ValidatePasswordAsync(memberUser, newPassword); + if (!validate.Succeeded) + { + return validate; + } + } + + var passwordStore = Store as IUserPasswordStore; + if (passwordStore == null) throw new NotSupportedException("The current user store does not implement " + typeof(IUserPasswordStore<>)); + + var hash = newPassword != null ? PasswordHasher.HashPassword(memberUser, newPassword) : null; + await passwordStore.SetPasswordHashAsync(memberUser, hash, CancellationToken); + await UpdateSecurityStampInternal(memberUser); + return IdentityResult.Success; + } + + ///TODO: duplicated code + /// + /// Logic used to validate a username and password + /// + /// + /// + /// + /// + /// By default this uses the standard ASP.Net Identity approach which is: + /// * Get password store + /// * Call VerifyPasswordAsync with the password store + user + password + /// * Uses the PasswordHasher.VerifyHashedPassword to compare the stored password + /// + /// In some cases people want simple custom control over the username/password check, for simplicity + /// sake, developers would like the users to simply validate against an LDAP directory but the user + /// data remains stored inside of Umbraco. + /// See: http://issues.umbraco.org/issue/U4-7032 for the use cases. + /// + /// We've allowed this check to be overridden with a simple callback so that developers don't actually + /// have to implement/override this class. + /// + public override async Task CheckPasswordAsync(T member, string password) + { + if (UmbracoMembersUserPasswordChecker != null) + { + UmbracoMembersUserPasswordCheckerResult result = await UmbracoMembersUserPasswordChecker.CheckPasswordAsync(member, password); + + if (member.HasIdentity == false) + { + return false; + } + + //if the result indicates to not fallback to the default, then return true if the credentials are valid + if (result != UmbracoMembersUserPasswordCheckerResult.FallbackToDefaultChecker) + { + return result == UmbracoMembersUserPasswordCheckerResult.ValidCredentials; + } + } + + //we cannot proceed if the user passed in does not have an identity + if (member.HasIdentity == false) + return false; + + //use the default behavior + return await base.CheckPasswordAsync(member, password); + } + + ///[TODO: from BackOfficeUserManager duplicated, could be shared] + /// + /// This is copied from the underlying .NET base class since they decided to not expose it + /// + /// + /// + private async Task UpdateSecurityStampInternal(T user) + { + if (SupportsUserSecurityStamp == false) return; + await GetSecurityStore().SetSecurityStampAsync(user, NewSecurityStamp(), CancellationToken.None); + } + + ///[TODO: from BackOfficeUserManager duplicated, could be shared] + /// + /// This is copied from the underlying .NET base class since they decided 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; + } + + ///[TODO: from BackOfficeUserManager duplicated, could be shared] + /// + /// This is copied from the underlying .NET base class since they decided to not expose it + /// + /// + private static string NewSecurityStamp() + { + return Guid.NewGuid().ToString(); + } + + ///[TODO: from BackOfficeUserManager duplicated, could be shared] + /// + /// Helper method to generate a password for a member based on the current password validator + /// + /// + public string GeneratePassword() + { + if (_passwordGenerator == null) + { + _passwordGenerator = new PasswordGenerator(PasswordConfiguration); + } + string password = _passwordGenerator.GeneratePassword(); + return password; } } } diff --git a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs index 10c85a9def..482d154a2e 100644 --- a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs +++ b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs @@ -1,16 +1,12 @@ using Microsoft.AspNetCore.Identity; using System; -using System.Collections.Generic; using System.Data; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Umbraco.Core; -using Umbraco.Core.BackOffice; +using Umbraco.Core.Mapping; using Umbraco.Core.Members; using Umbraco.Core.Models; -using Umbraco.Core.Models.Identity; -using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; namespace Umbraco.Infrastructure.Members @@ -19,8 +15,8 @@ namespace Umbraco.Infrastructure.Members /// A custom user store that uses Umbraco member data /// public class UmbracoMembersUserStore : DisposableObjectSlim, - IUserStore - //IUserPasswordStore + IUserStore, + IUserPasswordStore //IUserEmailStore //IUserLoginStore //IUserRoleStore, @@ -30,32 +26,37 @@ namespace Umbraco.Infrastructure.Members //IUserSessionStore { private bool _disposed = false; - private IMemberService _memberService; + private readonly IMemberService _memberService; + private readonly UmbracoMapper _mapper; - public UmbracoMembersUserStore(IMemberService memberService) + public UmbracoMembersUserStore(IMemberService memberService, UmbracoMapper mapper) { _memberService = memberService; + _mapper = mapper; } - public Task CreateAsync(UmbracoMembersIdentityUser memberUser, CancellationToken cancellationToken) { - //TODO: cancellationToken.ThrowIfCancellationRequested(); - //TODO: ThrowIfDisposed(); + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); if (memberUser == null) throw new ArgumentNullException(nameof(memberUser)); - // [Comments from Identity package and BackOfficeUser] + // [Comments from Identity package and BackOfficeUser - do we need this?] // 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(memberUser, Guid.NewGuid().ToString("N")); + if (memberUser.RawPasswordValue.IsNullOrWhiteSpace()) + { + memberUser.RawPasswordValue = emptyPasswordValue; + } + //create member //TODO: are we keeping this method, e.g. the Member Service? IMember memberEntity = _memberService.CreateMember( @@ -65,8 +66,7 @@ namespace Umbraco.Infrastructure.Members memberUser.MemberTypeAlias.IsNullOrWhiteSpace() ? Constants.Security.DefaultMemberTypeAlias : memberUser.MemberTypeAlias); - - UpdateMemberProperties(memberEntity, memberUser); + bool anythingChanged = UpdateMemberProperties(memberEntity, memberUser); _memberService.Save(memberEntity); @@ -115,7 +115,6 @@ namespace Umbraco.Infrastructure.Members //} } - private bool UpdateMemberProperties(IMember member, UmbracoMembersIdentityUser memberIdentityUser) { //[Comments as per BackOfficeUserStore & identity package] @@ -191,13 +190,15 @@ namespace Umbraco.Infrastructure.Members } //TODO: PasswordHash and PasswordConfig - //if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.PasswordHash)) - // && member.RawPasswordValue != identityUser.PasswordHash && identityUser.PasswordHash.IsNullOrWhiteSpace() == false) - //{ - // anythingChanged = true; - // member.RawPasswordValue = identityUser.PasswordHash; - // member.PasswordConfiguration = identityUser.PasswordConfig; - //} + if ( + //member.IsPropertyDirty(nameof(BackOfficeIdentityUser.PasswordHash))&& + member.RawPasswordValue != memberIdentityUser.PasswordHash + && memberIdentityUser.PasswordHash.IsNullOrWhiteSpace() == false) + { + anythingChanged = true; + member.RawPasswordValue = memberIdentityUser.PasswordHash; + member.PasswordConfiguration = memberIdentityUser.PasswordConfig; + } //TODO: SecurityStamp //if (member.SecurityStamp != identityUser.SecurityStamp) @@ -245,7 +246,6 @@ namespace Umbraco.Infrastructure.Members return anythingChanged; } - public Task DeleteAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) { throw new NotImplementedException(); @@ -256,9 +256,20 @@ namespace Umbraco.Infrastructure.Members throw new NotImplementedException(); } - public Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) + public async Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) { - throw new NotImplementedException(); + //TODO: confirm logic + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + var member = _memberService.GetByUsername(normalizedUserName); + if (member == null) + { + return null; + } + + var result = _mapper.Map(member); + + return await Task.FromResult(result); } public Task GetNormalizedUserNameAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) @@ -268,22 +279,36 @@ namespace Umbraco.Infrastructure.Members public Task GetUserIdAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) { - throw new NotImplementedException(); + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(user)); + + return Task.FromResult(user.Id.ToString()); } public Task GetUserNameAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) { - throw new NotImplementedException(); + //TODO: unit tests for and implement all bodies + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(user)); + + return Task.FromResult(user.UserName); } public Task SetNormalizedUserNameAsync(UmbracoMembersIdentityUser user, string normalizedName, CancellationToken cancellationToken) { - throw new NotImplementedException(); + return SetUserNameAsync(user, normalizedName, cancellationToken); throw new NotImplementedException(); } public Task SetUserNameAsync(UmbracoMembersIdentityUser user, string userName, CancellationToken cancellationToken) { - throw new NotImplementedException(); + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(user)); + + user.UserName = userName; + return Task.CompletedTask; } public Task UpdateAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) @@ -296,5 +321,57 @@ namespace Umbraco.Infrastructure.Members if (_disposed) throw new ObjectDisposedException(GetType().Name); } + + ///TODO: All from BackOfficeUserStore - same. Can we share? + /// + /// Set the user password hash + /// + /// + /// + /// + public Task SetPasswordHashAsync(UmbracoMembersIdentityUser user, string passwordHash, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(user)); + if (passwordHash == null) throw new ArgumentNullException(nameof(passwordHash)); + if (string.IsNullOrEmpty(passwordHash)) throw new ArgumentException("Value can't be empty.", nameof(passwordHash)); + + user.PasswordHash = passwordHash; + user.PasswordConfig = null; // Clear this so that it's reset at the repository level + + return Task.CompletedTask; + } + + /// + /// Get the user password hash + /// + /// + /// + /// + public Task GetPasswordHashAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(user)); + + return Task.FromResult(user.PasswordHash); + } + + /// + /// Returns true if a user has a password set + /// + /// + /// + /// + public Task HasPasswordAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(user)); + + return Task.FromResult(string.IsNullOrEmpty(user.PasswordHash) == false); + } + } } diff --git a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index 792b5cd5c1..0c3adae74b 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -134,6 +134,7 @@ namespace Umbraco.Tests.Integration.TestServerTest .WithRuntimeMinifier() .WithBackOffice() .WithBackOfficeIdentity() + .WithUmbracoMembersIdentity() .WithPreview() //.WithMiniProfiler() // we don't want this running in tests .WithMvcAndRazor(mvcBuilding: mvcBuilder => diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs index 450b3a341a..35d70d0915 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs @@ -37,6 +37,6 @@ namespace Umbraco.Tests.Integration.Umbraco.Web.BackOffice Assert.NotNull(userManager); } - protected override Action CustomTestSetup => (services) => services.AddUmbracoBackOfficeIdentity(); + protected override Action CustomTestSetup => (services) => services.AddUmbracoMembersIdentity(); } } diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoMembersServiceCollectionExtensionsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoMembersServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000000..0432ffa7db --- /dev/null +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoMembersServiceCollectionExtensionsTests.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Umbraco.Core.Members; +using Umbraco.Extensions; +using Umbraco.Infrastructure.Members; +using Umbraco.Tests.Integration.Testing; + +namespace Umbraco.Tests.Integration.Umbraco.Web.BackOffice +{ + [TestFixture] + public class UmbracoMembersServiceCollectionExtensionsTests : UmbracoIntegrationTest + { + [Test] + public void AddUmbracoMembersIdentity_ExpectUmbracoMembersUserStoreResolvable() + { + var userStore = Services.GetService>(); + + Assert.IsNotNull(userStore); + Assert.AreEqual(typeof(UmbracoMembersUserStore), userStore.GetType()); + } + + //[Test] + //public void AddUmbracoMembersIdentity_ExpectUmbracoMembersClaimsPrincipalFactoryResolvable() + //{ + // var principalFactory = Services.GetService>(); + + // Assert.IsNotNull(principalFactory); + // Assert.AreEqual(typeof(UmbracoMembersClaimsPrincipalFactory), principalFactory.GetType()); + //} + + [Test] + public void AddUmbracoMembersIdentity_ExpectUmbracomMembersUserManagerResolvable() + { + var userManager = Services.GetService(); + + Assert.NotNull(userManager); + } + + protected override Action CustomTestSetup => (services) => services.AddUmbracoMembersIdentity(); + } +} diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserManagerTests.cs new file mode 100644 index 0000000000..ca498bb70f --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserManagerTests.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using NUnit.Framework; +using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Members; +using Umbraco.Infrastructure.Members; + +namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Members +{ + [TestFixture] + public class UmbracoMemberIdentityUserManagerTests + { + private Mock> _mockMemberStore; + private Mock> _mockIdentityOptions; + private Mock> _mockPasswordHasher; + private Mock> _mockUserValidators; + private Mock>> _mockPasswordValidators; + private Mock _mockNormalizer; + private IdentityErrorDescriber _mockErrorDescriber; + private Mock _mockServiceProviders; + private Mock>> _mockLogger; + + public UmbracoMembersUserManager CreateSut() + { + _mockMemberStore = new Mock>(); + _mockIdentityOptions = new Mock>(); + + var idOptions = new UmbracoMembersIdentityOptions { Lockout = { AllowedForNewUsers = false } }; + _mockIdentityOptions.Setup(o => o.Value).Returns(idOptions); + _mockPasswordHasher = new Mock>(); + + var userValidators = new List>(); + _mockUserValidators = new Mock>(); + var validator = new Mock>(); + userValidators.Add(validator.Object); + + _mockPasswordValidators = new Mock>>(); + _mockNormalizer = new Mock(); + _mockErrorDescriber = new IdentityErrorDescriber(); + _mockServiceProviders = new Mock(); + _mockLogger = new Mock>>(); + + var pwdValidators = new List> + { + new PasswordValidator() + }; + + var userManager = new UmbracoMembersUserManager( + _mockMemberStore.Object, + _mockIdentityOptions.Object, + _mockPasswordHasher.Object, + userValidators, + pwdValidators, + new UpperInvariantLookupNormalizer(), + new IdentityErrorDescriber(), + _mockServiceProviders.Object, + new Mock>>().Object, + new Mock>().Object); + + validator.Setup(v => v.ValidateAsync( + userManager, + It.IsAny())) + .Returns(Task.FromResult(IdentityResult.Success)).Verifiable(); + + return userManager; + } + + [Test] + public async Task GivenICreateUser_AndTheIdentityResultFailed_ThenIShouldGetAFailedResultAsync() + { + //arrange + UmbracoMembersUserManager sut = CreateSut(); + UmbracoMembersIdentityUser fakeUser = new UmbracoMembersIdentityUser() { }; + CancellationToken fakeCancellationToken = new CancellationToken() { }; + IdentityError[] identityErrors = + { + new IdentityError() + { + Code = "IdentityError1", + Description = "There was an identity error when creating a user" + } + }; + + _mockMemberStore.Setup(x => + x.CreateAsync(fakeUser, fakeCancellationToken)) + .ReturnsAsync(IdentityResult.Failed(identityErrors)); + + //act + IdentityResult identityResult = await sut.CreateAsync(fakeUser); + + //assert + Assert.IsFalse(identityResult.Succeeded); + Assert.IsFalse(!identityResult.Errors.Any()); + + } + + + [Test] + public async Task GivenICreateUser_AndTheUserIsNull_ThenIShouldGetAFailedResultAsync() + { + //arrange + UmbracoMembersUserManager sut = CreateSut(); + UmbracoMembersIdentityUser fakeUser = new UmbracoMembersIdentityUser() { }; + CancellationToken fakeCancellationToken = new CancellationToken() { }; + IdentityError[] identityErrors = + { + new IdentityError() + { + Code = "IdentityError1", + Description = "There was an identity error when creating a user" + } + }; + + _mockMemberStore.Setup(x => + x.CreateAsync(null, fakeCancellationToken)) + .ReturnsAsync(IdentityResult.Failed(identityErrors)); + + //act + var identityResult = new Func>(() => sut.CreateAsync(null)); + + + //assert + Assert.That(identityResult, Throws.ArgumentNullException); + } + + + [Test] + public async Task GivenICreateANewUser_AndTheUserIsPopulatedCorrectly_ThenIShouldGetASuccessResultAsync() + { + //arrange + UmbracoMembersUserManager sut = CreateSut(); + UmbracoMembersIdentityUser fakeUser = new UmbracoMembersIdentityUser() { }; + CancellationToken fakeCancellationToken = new CancellationToken() { }; + _mockMemberStore.Setup(x => + x.CreateAsync(fakeUser, fakeCancellationToken)) + .ReturnsAsync(IdentityResult.Success); + + //act + IdentityResult identityResult = await sut.CreateAsync(fakeUser); + + //assert + Assert.IsTrue(identityResult.Succeeded); + Assert.IsTrue(!identityResult.Errors.Any()); + } + } +} diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserStoreTests.cs index 830ff4f14d..c2274d0132 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserStoreTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserStoreTests.cs @@ -1,11 +1,13 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Castle.Core.Internal; using Microsoft.AspNetCore.Identity; using Moq; using NUnit.Framework; +using Umbraco.Core.Mapping; using Umbraco.Core.Members; using Umbraco.Core.Models; using Umbraco.Core.Services; @@ -22,7 +24,10 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Members public UmbracoMembersUserStore CreateSut() { _mockMemberService = new Mock(); - return new UmbracoMembersUserStore(_mockMemberService.Object); + return new UmbracoMembersUserStore( + _mockMemberService.Object, + new UmbracoMapper(new MapDefinitionCollection( + new Mock>().Object))); } [Test] @@ -41,20 +46,15 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Members [Test] - public async Task GivenICreateUser_AndTheUserIsPopulatedCorrectly_ThenIShouldGetASuccessResult() + public async Task GivenICreateANewUser_AndTheUserIsPopulatedCorrectly_ThenIShouldGetASuccessResultAsync() { //arrange UmbracoMembersUserStore sut = CreateSut(); - UmbracoMembersIdentityUser fakeUser = new UmbracoMembersIdentityUser() - { - - }; - + UmbracoMembersIdentityUser fakeUser = new UmbracoMembersIdentityUser() { }; CancellationToken fakeCancellationToken = new CancellationToken() { }; IMemberType fakeMemberType = new MemberType(new MockShortStringHelper(), 77); - - var mockMember = Mock.Of(m => + IMember mockMember = Mock.Of(m => m.Name == "fakeName" && m.Email == "fakeemail@umbraco.com" && m.Username == "fakeUsername" && @@ -74,5 +74,23 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Members Assert.IsTrue(identityResult.Succeeded); Assert.IsTrue(!identityResult.Errors.Any()); } + + //FindByNameAsync + [Test] + public async Task GivenIGetUserNameAsync() + { + } + + [Test] + public async Task GivenIFindByNameAsync() + { + } + + //SetNormalizedUserNameAsync + //SetUserNameAsync + //HasPasswordAsync + //GetPasswordHashAsync + //SetPasswordHashAsync + //GetUserIdAsync } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index 55042d458b..92ff630023 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -7,6 +7,7 @@ using System.Net.Http; using System.Net.Mime; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -15,8 +16,10 @@ using Umbraco.Core.Configuration.Models; using Umbraco.Core.Dictionary; using Umbraco.Core.Events; using Umbraco.Core.Mapping; +using Umbraco.Core.Members; using Umbraco.Core.Models; using Umbraco.Core.Models.ContentEditing; +using Umbraco.Core.Models.Membership; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Security; using Umbraco.Core.Serialization; @@ -24,6 +27,7 @@ using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; using Umbraco.Core.Strings; using Umbraco.Extensions; +using Umbraco.Infrastructure.Members; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.BackOffice.ModelBinders; using Umbraco.Web.Common.Attributes; @@ -31,7 +35,6 @@ using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Common.Filters; using Umbraco.Web.ContentApps; using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Security; using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.BackOffice.Controllers @@ -45,12 +48,11 @@ namespace Umbraco.Web.BackOffice.Controllers [OutgoingNoHyphenGuidFormat] public class MemberController : ContentControllerBase { - private readonly MemberPasswordConfigurationSettings _passwordConfig; private readonly PropertyEditorCollection _propertyEditors; - private readonly LegacyPasswordSecurity _passwordSecurity; private readonly UmbracoMapper _umbracoMapper; private readonly IMemberService _memberService; private readonly IMemberTypeService _memberTypeService; + private readonly IUmbracoMembersUserManager _memberManager; private readonly IDataTypeService _dataTypeService; private readonly ILocalizedTextService _localizedTextService; private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; @@ -62,23 +64,21 @@ namespace Umbraco.Web.BackOffice.Controllers IShortStringHelper shortStringHelper, IEventMessagesFactory eventMessages, ILocalizedTextService localizedTextService, - IOptions passwordConfig, PropertyEditorCollection propertyEditors, - LegacyPasswordSecurity passwordSecurity, UmbracoMapper umbracoMapper, IMemberService memberService, IMemberTypeService memberTypeService, + IUmbracoMembersUserManager memberManager, IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backofficeSecurityAccessor, IJsonSerializer jsonSerializer) : base(cultureDictionary, loggerFactory, shortStringHelper, eventMessages, localizedTextService, jsonSerializer) { - _passwordConfig = passwordConfig.Value; _propertyEditors = propertyEditors; - _passwordSecurity = passwordSecurity; _umbracoMapper = umbracoMapper; _memberService = memberService; _memberTypeService = memberTypeService; + _memberManager = memberManager; _dataTypeService = dataTypeService; _localizedTextService = localizedTextService; _backofficeSecurityAccessor = backofficeSecurityAccessor; @@ -100,8 +100,15 @@ namespace Umbraco.Web.BackOffice.Controllers throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); } - var members = _memberService - .GetAll((pageNumber - 1), pageSize, out var totalRecords, orderBy, orderDirection, orderBySystemField, memberTypeAlias, filter).ToArray(); + IMember[] members = _memberService.GetAll( + pageNumber - 1, + pageSize, + out var totalRecords, + orderBy, + orderDirection, + orderBySystemField, + memberTypeAlias, + filter).ToArray(); if (totalRecords == 0) { return new PagedResult(0, 0, 0); @@ -109,8 +116,7 @@ namespace Umbraco.Web.BackOffice.Controllers var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize) { - Items = members - .Select(x => _umbracoMapper.Map(x)) + Items = members.Select(x => _umbracoMapper.Map(x)) }; return pagedResult; } @@ -125,8 +131,15 @@ namespace Umbraco.Web.BackOffice.Controllers var foundType = _memberTypeService.Get(listName); var name = foundType != null ? foundType.Name : listName; - var apps = new List(); - apps.Add(ListViewContentAppFactory.CreateContentApp(_dataTypeService, _propertyEditors, listName, "member", Core.Constants.DataTypes.DefaultMembersListView)); + var apps = new List + { + ListViewContentAppFactory.CreateContentApp( + _dataTypeService, + _propertyEditors, + listName, + Constants.Security.DefaultMemberTypeAlias.ToLower(), + Constants.DataTypes.DefaultMembersListView) + }; apps[0].Active = true; var display = new MemberListDisplay @@ -152,7 +165,7 @@ namespace Umbraco.Web.BackOffice.Controllers [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] public MemberDisplay GetByKey(Guid key) { - var foundMember = _memberService.GetByKey(key); + IMember foundMember = _memberService.GetByKey(key); if (foundMember == null) { HandleContentNotFound(key); @@ -168,22 +181,21 @@ namespace Umbraco.Web.BackOffice.Controllers [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] public MemberDisplay GetEmpty(string contentTypeAlias = null) { - IMember emptyContent; if (contentTypeAlias == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } - var contentType = _memberTypeService.Get(contentTypeAlias); + IMemberType contentType = _memberTypeService.Get(contentTypeAlias); if (contentType == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } - var passwordGenerator = new PasswordGenerator(_passwordConfig); + IMember emptyContent = new Member(contentType); - emptyContent = new Member(contentType); - emptyContent.AdditionalData["NewPassword"] = passwordGenerator.GeneratePassword(); + string newPassword = _memberManager.GeneratePassword(); + emptyContent.AdditionalData["NewPassword"] = newPassword; return _umbracoMapper.Map(emptyContent); } @@ -224,8 +236,10 @@ namespace Umbraco.Web.BackOffice.Controllers // their handlers. If we don't look this up now there's a chance we'll just end up // removing the roles they've assigned. var currRoles = _memberService.GetAllRoles(contentItem.PersistedContent.Username); + //find the ones to remove and remove them - var rolesToRemove = currRoles.Except(contentItem.Groups).ToArray(); + IEnumerable roles = currRoles.ToList(); + var rolesToRemove = roles.Except(contentItem.Groups).ToArray(); //Depending on the action we need to first do a create or update using the membership provider // this ensures that passwords are formatted correctly and also performs the validation on the provider itself. @@ -235,7 +249,7 @@ namespace Umbraco.Web.BackOffice.Controllers UpdateMemberData(contentItem); break; case ContentSaveAction.SaveNew: - contentItem.PersistedContent = CreateMemberData(contentItem); + contentItem.PersistedContent = await CreateMemberData(contentItem); break; default: //we don't support anything else for members @@ -255,7 +269,7 @@ namespace Umbraco.Web.BackOffice.Controllers _memberService.DissociateRoles(new[] { contentItem.PersistedContent.Username }, rolesToRemove); } //find the ones to add and add them - var toAdd = contentItem.Groups.Except(currRoles).ToArray(); + string[] toAdd = contentItem.Groups.Except(roles).ToArray(); if (toAdd.Any()) { //add the ones submitted @@ -263,18 +277,20 @@ namespace Umbraco.Web.BackOffice.Controllers } //return the updated model - var display = _umbracoMapper.Map(contentItem.PersistedContent); + MemberDisplay display = _umbracoMapper.Map(contentItem.PersistedContent); //lastly, if it is not valid, add the model state to the outgoing object and throw a 403 HandleInvalidModelState(display); - var localizedTextService = _localizedTextService; + ILocalizedTextService localizedTextService = _localizedTextService; //put the correct messages in switch (contentItem.Action) { case ContentSaveAction.Save: case ContentSaveAction.SaveNew: - display.AddSuccessNotification(localizedTextService.Localize("speechBubbles/editMemberSaved"), localizedTextService.Localize("speechBubbles/editMemberSaved")); + display.AddSuccessNotification( + localizedTextService.Localize("speechBubbles/editMemberSaved"), + localizedTextService.Localize("speechBubbles/editMemberSaved")); break; } @@ -302,36 +318,98 @@ namespace Umbraco.Web.BackOffice.Controllers null); // member are all invariant } - private IMember CreateMemberData(MemberSave contentItem) + /// + /// Create a member from the supplied member content data + /// All member password processing and creation is done via the aspnet identity MemberUserManager + /// + /// + /// + private async Task CreateMemberData(MemberSave memberSave) { - throw new NotImplementedException("Members have not been migrated to netcore"); + if (memberSave == null) throw new ArgumentNullException("memberSave"); - // TODO: all member password processing and creation needs to be done with a new aspnet identity MemberUserManager that hasn't been created yet. + if (ModelState.IsValid == false) + { + throw new HttpResponseException(HttpStatusCode.BadRequest, ModelState); + } + //TODO: check if unique - //var memberType = _memberTypeService.Get(contentItem.ContentTypeAlias); - //if (memberType == null) - // throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}"); - //var member = new Member(contentItem.Name, contentItem.Email, contentItem.Username, memberType, true) + IMemberType memberType = _memberTypeService.Get(memberSave.ContentTypeAlias); + if (memberType == null) + { + throw new InvalidOperationException($"No member type found with alias {memberSave.ContentTypeAlias}"); + } + + // Create the member with the UserManager + // The 'empty' (special) password format is applied without us having to duplicate that logic + UmbracoMembersIdentityUser identityMember = UmbracoMembersIdentityUser.CreateNew( + memberSave.Username, + memberSave.Email, + memberSave.Name); + + //TODO: confirm + identityMember.MemberTypeAlias = memberType.Alias; + + IdentityResult created = await _memberManager.CreateAsync(identityMember); + if (created.Succeeded == false) + { + throw HttpResponseException.CreateNotificationValidationErrorResponse(created.Errors.ToErrorMessage()); + } + + string resetPassword; + string password = _memberManager.GeneratePassword(); + + IdentityResult result = await _memberManager.AddPasswordAsync(identityMember, password); + if (result.Succeeded == false) + { + throw HttpResponseException.CreateNotificationValidationErrorResponse(created.Errors.ToErrorMessage()); + } + + resetPassword = password; + + //now re-look the member back up which will now exist + IMember member = _memberService.GetByEmail(memberSave.Email); + + //TODO: previous implementation + //IMember member = new Member( + // memberSave.Name, + // memberSave.Email, + // memberSave.Username, + // memberType, + // true) //{ - // CreatorId = _backofficeSecurityAccessor.BackofficeSecurity.CurrentUser.Id, - // RawPasswordValue = _passwordSecurity.HashPasswordForStorage(contentItem.Password.NewPassword), - // Comments = contentItem.Comments, - // IsApproved = contentItem.IsApproved + // CreatorId = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id, + // RawPasswordValue = _memberManager.GeneratePassword(), + // Comments = memberSave.Comments, + // IsApproved = memberSave.IsApproved //}; - //return member; + + //since the back office user is creating this member, they will be set to approved + member.IsApproved = true; + member.CreatorId = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id; + member.Comments = memberSave.Comments; + member.IsApproved = memberSave.IsApproved; + + //map the save info over onto the user + member = _umbracoMapper.Map(memberSave, member); + + _memberService.Save(member); + + return member; } /// /// Update the member security data /// - /// + /// /// /// If the password has been reset then this method will return the reset/generated password, otherwise will return null. /// - private void UpdateMemberData(MemberSave contentItem) + private void UpdateMemberData(MemberSave memberSave) { - contentItem.PersistedContent.WriterId = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id; + //TODO: optimise based on new member manager + memberSave.PersistedContent.WriterId = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id; // If the user doesn't have access to sensitive values, then we need to check if any of the built in member property types // have been marked as sensitive. If that is the case we cannot change these persisted values no matter what value has been posted. @@ -339,32 +417,32 @@ namespace Umbraco.Web.BackOffice.Controllers // but we will take care of this in a generic way below so that it works for all props. if (!_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.HasAccessToSensitiveData()) { - var memberType = _memberTypeService.Get(contentItem.PersistedContent.ContentTypeId); + var memberType = _memberTypeService.Get(memberSave.PersistedContent.ContentTypeId); var sensitiveProperties = memberType .PropertyTypes.Where(x => memberType.IsSensitiveProperty(x.Alias)) .ToList(); foreach (var sensitiveProperty in sensitiveProperties) { - var destProp = contentItem.Properties.FirstOrDefault(x => x.Alias == sensitiveProperty.Alias); + var destProp = memberSave.Properties.FirstOrDefault(x => x.Alias == sensitiveProperty.Alias); if (destProp != null) { //if found, change the value of the contentItem model to the persisted value so it remains unchanged - var origValue = contentItem.PersistedContent.GetValue(sensitiveProperty.Alias); + var origValue = memberSave.PersistedContent.GetValue(sensitiveProperty.Alias); destProp.Value = origValue; } } } - var isLockedOut = contentItem.IsLockedOut; + var isLockedOut = memberSave.IsLockedOut; //if they were locked but now they are trying to be unlocked - if (contentItem.PersistedContent.IsLockedOut && isLockedOut == false) + if (memberSave.PersistedContent.IsLockedOut && isLockedOut == false) { - contentItem.PersistedContent.IsLockedOut = false; - contentItem.PersistedContent.FailedPasswordAttempts = 0; + memberSave.PersistedContent.IsLockedOut = false; + memberSave.PersistedContent.FailedPasswordAttempts = 0; } - else if (!contentItem.PersistedContent.IsLockedOut && isLockedOut) + else if (!memberSave.PersistedContent.IsLockedOut && isLockedOut) { //NOTE: This should not ever happen unless someone is mucking around with the request data. //An admin cannot simply lock a user, they get locked out by password attempts, but an admin can un-approve them @@ -372,13 +450,11 @@ namespace Umbraco.Web.BackOffice.Controllers } //no password changes then exit ? - if (contentItem.Password == null) + if (memberSave.Password == null) return; - throw new NotImplementedException("Members have not been migrated to netcore"); - // TODO: all member password processing and creation needs to be done with a new aspnet identity MemberUserManager that hasn't been created yet. // set the password - //contentItem.PersistedContent.RawPasswordValue = _passwordSecurity.HashPasswordForStorage(contentItem.Password.NewPassword); + memberSave.PersistedContent.RawPasswordValue = _memberManager.GeneratePassword(); } private static void UpdateName(MemberSave memberSave) @@ -396,23 +472,23 @@ namespace Umbraco.Web.BackOffice.Controllers if (contentItem.Name.IsNullOrWhiteSpace()) { ModelState.AddPropertyError( - new ValidationResult("Invalid user name", new[] { "value" }), - string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + new ValidationResult("Invalid user name", new[] { "value" }), + $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login"); return false; } if (contentItem.Password != null && !contentItem.Password.NewPassword.IsNullOrWhiteSpace()) { - //TODO implement when NETCORE members are implemented - throw new NotImplementedException("TODO implement when members are implemented"); - // var validPassword = await _passwordValidator.ValidateAsync(_passwordConfig, contentItem.Password.NewPassword); - // if (!validPassword) - // { - // ModelState.AddPropertyError( - // new ValidationResult("Invalid password: " + string.Join(", ", validPassword.Result), new[] { "value" }), - // string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - // return false; - // } + //TODO: implement as per backoffice user + //var validPassword = await _memberManager.CheckPasswordAsync(null, contentItem.Password.NewPassword); + //if (!validPassword) + //{ + // ModelState.AddPropertyError( + // new ValidationResult("Invalid password: TODO", new[] { "value" }), + // $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password"); + // return false; + //} + return true; } var byUsername = _memberService.GetByUsername(contentItem.Username); @@ -420,7 +496,7 @@ namespace Umbraco.Web.BackOffice.Controllers { ModelState.AddPropertyError( new ValidationResult("Username is already in use", new[] { "value" }), - string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login"); return false; } @@ -429,7 +505,7 @@ namespace Umbraco.Web.BackOffice.Controllers { ModelState.AddPropertyError( new ValidationResult("Email address is already in use", new[] { "value" }), - string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email"); return false; } @@ -470,7 +546,7 @@ namespace Umbraco.Web.BackOffice.Controllers return Forbid(); } - var member = ((MemberService)_memberService).ExportMember(key); + MemberExportModel member = ((MemberService)_memberService).ExportMember(key); if (member is null) throw new NullReferenceException("No member found with key " + key); var json = _jsonSerializer.Serialize(member); @@ -479,9 +555,7 @@ namespace Umbraco.Web.BackOffice.Controllers // Set custom header so umbRequestHelper.downloadFile can save the correct filename HttpContext.Response.Headers.Add("x-filename", fileName); - return File( Encoding.UTF8.GetBytes(json), MediaTypeNames.Application.Octet, fileName); + return File(Encoding.UTF8.GetBytes(json), MediaTypeNames.Application.Octet, fileName); } } - - } diff --git a/src/Umbraco.Web.BackOffice/Extensions/IdentityBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/IdentityBuilderExtensions.cs index eab7142665..04930180d9 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/IdentityBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/IdentityBuilderExtensions.cs @@ -12,7 +12,8 @@ namespace Umbraco.Extensions /// The type of the user manager to add. /// /// The current instance. - public static IdentityBuilder AddUserManager(this IdentityBuilder identityBuilder) where TUserManager : UserManager, TInterface + public static IdentityBuilder AddUserManager(this IdentityBuilder identityBuilder) + where TUserManager : UserManager, TInterface { identityBuilder.AddUserManager(); identityBuilder.Services.AddScoped(typeof(TInterface), typeof(TUserManager)); diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs index c494425274..0bb5e922e4 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs @@ -13,6 +13,7 @@ namespace Umbraco.Extensions .WithRuntimeMinifier() .WithBackOffice() .WithBackOfficeIdentity() + .WithUmbracoMembersIdentity() .WithMiniProfiler() .WithMvcAndRazor() .WithWebServer() @@ -27,6 +28,9 @@ namespace Umbraco.Extensions public static IUmbracoBuilder WithBackOfficeIdentity(this IUmbracoBuilder builder) => builder.AddWith(nameof(WithBackOfficeIdentity), () => builder.Services.AddUmbracoBackOfficeIdentity()); + public static IUmbracoBuilder WithUmbracoMembersIdentity(this IUmbracoBuilder builder) + => builder.AddWith(nameof(WithUmbracoMembersIdentity), () => builder.Services.AddUmbracoMembersIdentity()); + public static IUmbracoBuilder WithPreview(this IUmbracoBuilder builder) => builder.AddWith(nameof(WithPreview), () => builder.Services.AddUmbracoPreview()); } diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoMemberIdentityBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoMemberIdentityBuilderExtensions.cs new file mode 100644 index 0000000000..bc8d2bcbfb --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoMemberIdentityBuilderExtensions.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Core.Members; + +namespace Umbraco.Extensions +{ + public static class UmbracoMemberIdentityBuilderExtensions + { + + /// + /// Adds a for the . + /// + /// The type of the user manager to add. + /// + /// The current instance. + public static IdentityBuilder AddUserManager(this IdentityBuilder identityBuilder) where TUserManager : UserManager, TInterface + { + identityBuilder.AddUserManager(); + identityBuilder.Services.AddScoped(typeof(TInterface), typeof(TUserManager)); + return identityBuilder; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoMembersUserServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoMembersUserServiceCollectionExtensions.cs new file mode 100644 index 0000000000..6687c9e5be --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoMembersUserServiceCollectionExtensions.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Umbraco.Core.Members; +using Umbraco.Core.Security; +using Umbraco.Core.Serialization; +using Umbraco.Infrastructure.Members; +using Umbraco.Web.BackOffice.Security; + +namespace Umbraco.Extensions +{ + public static class UmbracoMembersUserServiceCollectionExtensions + { + /// + /// Adds the services required for using Umbraco Members Identity + /// + /// + public static void AddUmbracoMembersIdentity(this IServiceCollection services) + { + services.BuildUmbracoMembersIdentity() + .AddDefaultTokenProviders() + .AddUserStore() + .AddUserManager(); + } + + private static UmbracoMembersIdentityBuilder BuildUmbracoMembersIdentity(this IServiceCollection services) + { + // Services used by Umbraco members identity + services.TryAddScoped, UserValidator>(); + services.TryAddScoped, PasswordValidator>(); + services.TryAddScoped, PasswordHasher>(); + return new UmbracoMembersIdentityBuilder(services); + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs index 31ab66908c..da77f041aa 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs @@ -69,6 +69,7 @@ namespace Umbraco.Web.BackOffice.Trees var node = GetSingleTreeNode(id, queryStrings); //add the tree alias to the node since it is standalone (has no root for which this normally belongs) + //TODO: ID is null since new member created node.Value.AdditionalData["treeAlias"] = TreeAlias; return node; } From 17df4eb607e3bf0996e8280c91a0d9978e96ca13 Mon Sep 17 00:00:00 2001 From: emmagarland Date: Mon, 30 Nov 2020 01:07:02 +0000 Subject: [PATCH 05/72] Members mapping definition updated, and commented out duplicate password work --- .../Models/Mapping/MemberMapDefinition.cs | 30 +++++++++++++++++++ .../CoreMappingProfiles.cs | 1 + .../Controllers/MemberController.cs | 16 +++++----- 3 files changed, 39 insertions(+), 8 deletions(-) create mode 100644 src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs diff --git a/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs new file mode 100644 index 0000000000..dad995bdac --- /dev/null +++ b/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs @@ -0,0 +1,30 @@ +using Umbraco.Core.Mapping; +using Umbraco.Core.Models; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Models.Mapping +{ + public class MemberMapDefinition : IMapDefinition + { + public MemberMapDefinition() + { + } + + public void DefineMaps(UmbracoMapper mapper) + { + mapper.Define(Map); + } + + // mappers + private static void Map(MemberSave source, IMember target, MapperContext context) + { + target.IsApproved = source.IsApproved; + target.Name = source.Name; + target.Email = source.Email; + target.Key = source.Key; + target.Username = source.Username; + target.Id = (int)(long)source.Id; + //TODO: map more properties as required + } + } +} diff --git a/src/Umbraco.Infrastructure/Composing/CompositionExtensions/CoreMappingProfiles.cs b/src/Umbraco.Infrastructure/Composing/CompositionExtensions/CoreMappingProfiles.cs index 823f8618ad..e35b936199 100644 --- a/src/Umbraco.Infrastructure/Composing/CompositionExtensions/CoreMappingProfiles.cs +++ b/src/Umbraco.Infrastructure/Composing/CompositionExtensions/CoreMappingProfiles.cs @@ -33,6 +33,7 @@ namespace Umbraco.Core.Composing.CompositionExtensions .Add() .Add() .Add() + .Add() .Add() .Add() ; diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index 92ff630023..dc7c2ce39c 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -356,16 +356,16 @@ namespace Umbraco.Web.BackOffice.Controllers throw HttpResponseException.CreateNotificationValidationErrorResponse(created.Errors.ToErrorMessage()); } - string resetPassword; - string password = _memberManager.GeneratePassword(); + //string resetPassword; + //string password = _memberManager.GeneratePassword(); - IdentityResult result = await _memberManager.AddPasswordAsync(identityMember, password); - if (result.Succeeded == false) - { - throw HttpResponseException.CreateNotificationValidationErrorResponse(created.Errors.ToErrorMessage()); - } + //IdentityResult result = await _memberManager.AddPasswordAsync(identityMember, password); + //if (result.Succeeded == false) + //{ + // throw HttpResponseException.CreateNotificationValidationErrorResponse(created.Errors.ToErrorMessage()); + //} - resetPassword = password; + //resetPassword = password; //now re-look the member back up which will now exist IMember member = _memberService.GetByEmail(memberSave.Email); From 9c73429da7e21f2adc95ab9562e04d75dc28772d Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Tue, 1 Dec 2020 18:19:36 +0000 Subject: [PATCH 06/72] Merge branch 'netcore/netcore' into netcore/members-userstore # Conflicts: # src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs # src/Umbraco.Web.BackOffice/Controllers/MemberController.cs # src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs --- .../Extensions/UmbracoBuilderExtensions.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs index 198f0c10d8..d02c4c1235 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs @@ -44,12 +44,15 @@ namespace Umbraco.Extensions }); // TODO: Need to add more cookie options, see https://github.com/dotnet/aspnetcore/blob/3.0/src/Identity/Core/src/IdentityServiceCollectionExtensions.cs#L45 + + builder.Services.ConfigureOptions(); return builder; } public static IUmbracoBuilder AddUmbracoMembersIdentity(this IUmbracoBuilder builder) { - builder.Services.ConfigureOptions(); + builder.Services.AddUmbracoMembersIdentity(); + return builder; } From f0f17b3cf83774f51bd6403d1be10835dcb2ba57 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Tue, 1 Dec 2020 19:06:36 +0000 Subject: [PATCH 07/72] Stop duplicate saving --- .../Members/UmbracoMembersUserStore.cs | 1 + src/Umbraco.Web.BackOffice/Controllers/MemberController.cs | 7 ++----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs index 482d154a2e..fcefd4b751 100644 --- a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs +++ b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs @@ -65,6 +65,7 @@ namespace Umbraco.Infrastructure.Members memberUser.Name.IsNullOrWhiteSpace() ? memberUser.UserName : memberUser.Name, memberUser.MemberTypeAlias.IsNullOrWhiteSpace() ? Constants.Security.DefaultMemberTypeAlias : memberUser.MemberTypeAlias); + //IMember member = new Member(memberUser.Name, memberUser.Email.ToLower().Trim(), memberUser.UserName, null); bool anythingChanged = UpdateMemberProperties(memberEntity, memberUser); diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index 3d7bd5288a..971716a239 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -261,8 +261,8 @@ namespace Umbraco.Web.BackOffice.Controllers //TODO: There's 3 things saved here and we should do this all in one transaction, which we can do here by wrapping in a scope // but it would be nicer to have this taken care of within the Save method itself - //create/save the IMember - _memberService.Save(contentItem.PersistedContent); + //TODO: create/save the IMember: this is now saved in CreateAsync, remove once logic is clarified + //_memberService.Save(contentItem.PersistedContent); //Now let's do the role provider stuff - now that we've saved the content item (that is important since // if we are changing the username, it must be persisted before looking up the member roles). @@ -395,9 +395,6 @@ namespace Umbraco.Web.BackOffice.Controllers //map the save info over onto the user member = _umbracoMapper.Map(memberSave, member); - - _memberService.Save(member); - return member; } From 75bee025ce60113a57d615b29d27ca3cfa3163c0 Mon Sep 17 00:00:00 2001 From: emmagarland Date: Tue, 1 Dec 2020 19:47:56 +0000 Subject: [PATCH 08/72] Fixed tests --- .../UmbracoMemberIdentityUserManagerTests.cs | 20 +++++++++++++++---- .../UmbracoMemberIdentityUserStoreTests.cs | 3 +-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserManagerTests.cs index ca498bb70f..0645590b38 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserManagerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserManagerTests.cs @@ -26,6 +26,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Members private IdentityErrorDescriber _mockErrorDescriber; private Mock _mockServiceProviders; private Mock>> _mockLogger; + private Mock> _mockPasswordConfiguration; public UmbracoMembersUserManager CreateSut() { @@ -46,6 +47,12 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Members _mockErrorDescriber = new IdentityErrorDescriber(); _mockServiceProviders = new Mock(); _mockLogger = new Mock>>(); + _mockPasswordConfiguration = new Mock>(); + _mockPasswordConfiguration.Setup(x => x.Value).Returns(() => + new MemberPasswordConfigurationSettings() + { + + }); var pwdValidators = new List> { @@ -62,7 +69,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Members new IdentityErrorDescriber(), _mockServiceProviders.Object, new Mock>>().Object, - new Mock>().Object); + _mockPasswordConfiguration.Object); validator.Setup(v => v.ValidateAsync( userManager, @@ -77,7 +84,10 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Members { //arrange UmbracoMembersUserManager sut = CreateSut(); - UmbracoMembersIdentityUser fakeUser = new UmbracoMembersIdentityUser() { }; + UmbracoMembersIdentityUser fakeUser = new UmbracoMembersIdentityUser() + { + PasswordConfig = "testConfig" + }; CancellationToken fakeCancellationToken = new CancellationToken() { }; IdentityError[] identityErrors = { @@ -107,7 +117,6 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Members { //arrange UmbracoMembersUserManager sut = CreateSut(); - UmbracoMembersIdentityUser fakeUser = new UmbracoMembersIdentityUser() { }; CancellationToken fakeCancellationToken = new CancellationToken() { }; IdentityError[] identityErrors = { @@ -136,7 +145,10 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Members { //arrange UmbracoMembersUserManager sut = CreateSut(); - UmbracoMembersIdentityUser fakeUser = new UmbracoMembersIdentityUser() { }; + UmbracoMembersIdentityUser fakeUser = new UmbracoMembersIdentityUser() + { + PasswordConfig = "testConfig" + }; CancellationToken fakeCancellationToken = new CancellationToken() { }; _mockMemberStore.Setup(x => x.CreateAsync(fakeUser, fakeCancellationToken)) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserStoreTests.cs index c2274d0132..59334d763c 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserStoreTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserStoreTests.cs @@ -26,8 +26,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Members _mockMemberService = new Mock(); return new UmbracoMembersUserStore( _mockMemberService.Object, - new UmbracoMapper(new MapDefinitionCollection( - new Mock>().Object))); + new UmbracoMapper(new MapDefinitionCollection(new List()))); } [Test] From fba4126a623d638df1d6b274c71d155e6579c72b Mon Sep 17 00:00:00 2001 From: emmagarland Date: Tue, 1 Dec 2020 20:41:12 +0000 Subject: [PATCH 09/72] Switched back to correct services --- .../UmbracoBackOfficeServiceCollectionExtensionsTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs index 35d70d0915..450b3a341a 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs @@ -37,6 +37,6 @@ namespace Umbraco.Tests.Integration.Umbraco.Web.BackOffice Assert.NotNull(userManager); } - protected override Action CustomTestSetup => (services) => services.AddUmbracoMembersIdentity(); + protected override Action CustomTestSetup => (services) => services.AddUmbracoBackOfficeIdentity(); } } From cf56731e4205fa27567cd00319bb09442c5ccfc4 Mon Sep 17 00:00:00 2001 From: emmagarland Date: Tue, 1 Dec 2020 20:44:21 +0000 Subject: [PATCH 10/72] Reverted back so avoid unwanted diffs --- .../Extensions/IdentityBuilderExtensions.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Extensions/IdentityBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/IdentityBuilderExtensions.cs index 04930180d9..eab7142665 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/IdentityBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/IdentityBuilderExtensions.cs @@ -12,8 +12,7 @@ namespace Umbraco.Extensions /// The type of the user manager to add. /// /// The current instance. - public static IdentityBuilder AddUserManager(this IdentityBuilder identityBuilder) - where TUserManager : UserManager, TInterface + public static IdentityBuilder AddUserManager(this IdentityBuilder identityBuilder) where TUserManager : UserManager, TInterface { identityBuilder.AddUserManager(); identityBuilder.Services.AddScoped(typeof(TInterface), typeof(TUserManager)); From 8a552a10d9e9554983f5118746a593f1b5debddc Mon Sep 17 00:00:00 2001 From: emmagarland Date: Tue, 1 Dec 2020 20:51:13 +0000 Subject: [PATCH 11/72] Added comments to understand where to implement --- src/Umbraco.Web.BackOffice/Controllers/MemberController.cs | 1 + src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index 971716a239..1e1ae5ff0f 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -167,6 +167,7 @@ namespace Umbraco.Web.BackOffice.Controllers [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] public MemberDisplay GetByKey(Guid key) { + //TODO: this is not finding the key currently IMember foundMember = _memberService.GetByKey(key); if (foundMember == null) { diff --git a/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs index 05a6e70191..4ebd8f7cc5 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs @@ -66,7 +66,6 @@ namespace Umbraco.Web.BackOffice.Trees var node = GetSingleTreeNode(id, queryStrings); //add the tree alias to the node since it is standalone (has no root for which this normally belongs) - //TODO: ID is null since new member created node.Value.AdditionalData["treeAlias"] = TreeAlias; return node; } From 55ddc8cc49687b87b36d98d158da2865f6ceed9f Mon Sep 17 00:00:00 2001 From: emmagarland Date: Tue, 1 Dec 2020 21:31:37 +0000 Subject: [PATCH 12/72] Reordered to ensure password isn't always empty, and renamed for clarity s per UmbracoIdentity --- .../Members/UmbracoMembersUserStore.cs | 57 +++++++++---------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs index fcefd4b751..aedf1e851d 100644 --- a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs +++ b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs @@ -35,44 +35,41 @@ namespace Umbraco.Infrastructure.Members _mapper = mapper; } - public Task CreateAsync(UmbracoMembersIdentityUser memberUser, CancellationToken cancellationToken) + public Task CreateAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - if (memberUser == null) throw new ArgumentNullException(nameof(memberUser)); - - - // [Comments from Identity package and BackOfficeUser - do we need this?] - // 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(memberUser, Guid.NewGuid().ToString("N")); - - if (memberUser.RawPasswordValue.IsNullOrWhiteSpace()) - { - memberUser.RawPasswordValue = emptyPasswordValue; - } + if (user == null) throw new ArgumentNullException(nameof(user)); //create member - //TODO: are we keeping this method, e.g. the Member Service? - IMember memberEntity = _memberService.CreateMember( - memberUser.UserName, - memberUser.Email, - memberUser.Name.IsNullOrWhiteSpace() ? memberUser.UserName : memberUser.Name, - memberUser.MemberTypeAlias.IsNullOrWhiteSpace() ? - Constants.Security.DefaultMemberTypeAlias : memberUser.MemberTypeAlias); - //IMember member = new Member(memberUser.Name, memberUser.Email.ToLower().Trim(), memberUser.UserName, null); + //TODO: are we keeping this method, e.g. the Member Service? The user service creates it directly, but this gets the membertype + IMember member = _memberService.CreateMember( + user.UserName, + user.Email, + user.Name.IsNullOrWhiteSpace() ? user.UserName : user.Name, + user.MemberTypeAlias.IsNullOrWhiteSpace() ? + Constants.Security.DefaultMemberTypeAlias : user.MemberTypeAlias); - bool anythingChanged = UpdateMemberProperties(memberEntity, memberUser); + UpdateMemberProperties(member, user); - _memberService.Save(memberEntity); + if (member.RawPasswordValue.IsNullOrWhiteSpace()) + { + // [Comments from Identity package and BackOfficeUser - can/should we share this functionality] + // 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(user, Guid.NewGuid().ToString("N")); + member.RawPasswordValue = emptyPasswordValue; + } + + _memberService.Save(member); //re-assign id - memberUser.Id = memberEntity.Id; + user.Id = member.Id; // TODO: do we need this? // TODO: [from backofficeuser] we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. @@ -88,7 +85,7 @@ namespace Umbraco.Infrastructure.Members // x.UserData))); //} - if (!memberEntity.HasIdentity) throw new DataException("Could not create the user, check logs for details"); + if (!member.HasIdentity) throw new DataException("Could not create the user, check logs for details"); return Task.FromResult(IdentityResult.Success); From f1fbeb8ad372d9b051563082efa9f480565a50a5 Mon Sep 17 00:00:00 2001 From: emmagarland Date: Thu, 3 Dec 2020 01:27:54 +0000 Subject: [PATCH 13/72] Set hasIdentity if there is an ID, and logical adjustments to set passwords corectly --- .../MemberPasswordConfiguration.cs | 2 +- .../Members/UmbracoMembersIdentityUser.cs | 23 +- .../Models/Mapping/MemberMapDefinition.cs | 6 +- .../Members/IUmbracoMembersUserManager.cs | 5 +- .../Members/UmbracoMembersUserManager.cs | 2 +- .../Members/UmbracoMembersUserStore.cs | 14 +- .../Controllers/MemberController.cs | 227 +++++++----------- 7 files changed, 115 insertions(+), 164 deletions(-) diff --git a/src/Umbraco.Core/Configuration/MemberPasswordConfiguration.cs b/src/Umbraco.Core/Configuration/MemberPasswordConfiguration.cs index 8e7cd97f35..b56a6e7272 100644 --- a/src/Umbraco.Core/Configuration/MemberPasswordConfiguration.cs +++ b/src/Umbraco.Core/Configuration/MemberPasswordConfiguration.cs @@ -3,7 +3,7 @@ namespace Umbraco.Core.Configuration { /// - /// The password configuration for back office users + /// The password configuration for members /// public class MemberPasswordConfiguration : PasswordConfiguration, IMemberPasswordConfiguration { diff --git a/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs b/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs index f3d5661926..89ede9d4a6 100644 --- a/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs +++ b/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs @@ -13,14 +13,13 @@ namespace Umbraco.Core.Members //: IdentityUser, IdentityUserClaim>, { private bool _hasIdentity; + private int _id; - public int Id { get; set; } public string Name { get; set; } public string Email { get; set; } public string UserName { get; set; } public string MemberTypeAlias { get; set; } public bool IsLockedOut { get; set; } - public string RawPasswordValue { get; set; } public DateTime LastPasswordChangeDateUtc { get; set; } @@ -30,6 +29,16 @@ namespace Umbraco.Core.Members /// public bool HasIdentity => _hasIdentity; + public int Id + { + get => _id; + set + { + _id = value; + _hasIdentity = true; + } + } + //TODO: track public string PasswordHash { get; set; } @@ -48,7 +57,11 @@ namespace Umbraco.Core.Members //public bool RolesChanged; - public static UmbracoMembersIdentityUser CreateNew(string username, string email, string name = null) + public static UmbracoMembersIdentityUser CreateNew( + string username, + string email, + string memberTypeAlias, + string name) { if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username)); @@ -58,7 +71,9 @@ namespace Umbraco.Core.Members UserName = username, Email = email, Name = name, - Id = 0, //TODO + MemberTypeAlias = memberTypeAlias, + Id = 0, //TODO: is this meant to be 0 in this circumstance? + //false by default unless specifically set _hasIdentity = false }; diff --git a/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs index dad995bdac..bc8589e8ef 100644 --- a/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs @@ -24,7 +24,11 @@ namespace Umbraco.Web.Models.Mapping target.Key = source.Key; target.Username = source.Username; target.Id = (int)(long)source.Id; - //TODO: map more properties as required + target.Comments = source.Comments; + target.IsApproved = source.IsApproved; + + //TODO: ensure all properties are mapped as required + } } } diff --git a/src/Umbraco.Infrastructure/Members/IUmbracoMembersUserManager.cs b/src/Umbraco.Infrastructure/Members/IUmbracoMembersUserManager.cs index 81b11f5141..02c1436a44 100644 --- a/src/Umbraco.Infrastructure/Members/IUmbracoMembersUserManager.cs +++ b/src/Umbraco.Infrastructure/Members/IUmbracoMembersUserManager.cs @@ -12,15 +12,16 @@ namespace Umbraco.Infrastructure.Members public interface IUmbracoMembersUserManager : IDisposable where TUser : UmbracoMembersIdentityUser { /// - /// Creates the specified in the backing store with no password, + /// Creates the specified in the backing store with a password, /// as an asynchronous operation. /// /// The member to create. + /// The password to add /// /// The that represents the asynchronous operation, containing the /// of the operation. /// - Task CreateAsync(TUser memberUser); + Task CreateAsync(TUser memberUser, string password); /// /// Helper method to generate a password for a user based on the current password validator diff --git a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserManager.cs b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserManager.cs index 717d054120..ce6e20210e 100644 --- a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserManager.cs +++ b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserManager.cs @@ -107,7 +107,7 @@ namespace Umbraco.Infrastructure.Members return IdentityResult.Success; } - ///TODO: duplicated code + ///TODO: duplicated code from backofficeusermanager /// /// Logic used to validate a username and password /// diff --git a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs index aedf1e851d..ba7d5d9324 100644 --- a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs +++ b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs @@ -52,19 +52,7 @@ namespace Umbraco.Infrastructure.Members UpdateMemberProperties(member, user); - if (member.RawPasswordValue.IsNullOrWhiteSpace()) - { - // [Comments from Identity package and BackOfficeUser - can/should we share this functionality] - // 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(user, Guid.NewGuid().ToString("N")); - member.RawPasswordValue = emptyPasswordValue; - } + //TODO: do we want to accept empty passwords here - if thirdparty for example? In other method if so? _memberService.Save(member); diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index 1e1ae5ff0f..a6d440d505 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -11,13 +11,10 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using Umbraco.Core; -using Umbraco.Core.Configuration.Models; using Umbraco.Core.Dictionary; using Umbraco.Core.Events; using Umbraco.Core.Mapping; -using Umbraco.Core.Members; using Umbraco.Core.Models; using Umbraco.Core.Models.ContentEditing; using Umbraco.Core.Models.Membership; @@ -38,6 +35,7 @@ using Umbraco.Web.Common.Filters; using Umbraco.Web.ContentApps; using Umbraco.Web.Models.ContentEditing; using Constants = Umbraco.Core.Constants; +using UmbracoMembersIdentityUser = Umbraco.Core.Members.UmbracoMembersIdentityUser; namespace Umbraco.Web.BackOffice.Controllers { @@ -213,6 +211,12 @@ namespace Umbraco.Web.BackOffice.Controllers [ModelBinder(typeof(MemberBinder))] MemberSave contentItem) { + if (contentItem == null) throw new ArgumentNullException(nameof(contentItem)); + + if (ModelState.IsValid == false) + { + throw new HttpResponseException(HttpStatusCode.BadRequest, ModelState); + } //If we've reached here it means: // * Our model has been bound @@ -224,7 +228,44 @@ namespace Umbraco.Web.BackOffice.Controllers //map the properties to the persisted entity MapPropertyValues(contentItem); - await ValidateMemberDataAsync(contentItem); + var memberType = _memberTypeService.Get(contentItem.ContentTypeAlias); + if (memberType == null) + { + throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}"); + } + + if (contentItem.Name.IsNullOrWhiteSpace()) + { + ModelState.AddPropertyError( + new ValidationResult("Invalid user name", new[] { "value" }), + $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login"); + } + + IMember byUsername = _memberService.GetByUsername(contentItem.Username); + if (byUsername != null && byUsername.Key != contentItem.Key) + { + ModelState.AddPropertyError( + new ValidationResult("Username is already in use", new[] { "value" }), + $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login"); + } + + IMember byEmail = _memberService.GetByEmail(contentItem.Email); + if (byEmail != null && byEmail.Key != contentItem.Key) + { + ModelState.AddPropertyError( + new ValidationResult("Email address is already in use", new[] { "value" }), + $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email"); + } + + // Create the member with the MemberManager + var identityMember = UmbracoMembersIdentityUser.CreateNew( + contentItem.Username, + contentItem.Email, + memberType.Alias, + contentItem.Name); + + //TODO: confirm where to do this + identityMember.RawPasswordValue = contentItem.Password.NewPassword; //Unlike content/media - if there are errors for a member, we do NOT proceed to save them, we cannot so return the errors if (ModelState.IsValid == false) @@ -238,21 +279,21 @@ namespace Umbraco.Web.BackOffice.Controllers // events to be raised and developers could be manually adding roles to members in // their handlers. If we don't look this up now there's a chance we'll just end up // removing the roles they've assigned. - var currRoles = _memberService.GetAllRoles(contentItem.PersistedContent.Username); + IEnumerable currRoles = _memberService.GetAllRoles(contentItem.PersistedContent.Username); //find the ones to remove and remove them IEnumerable roles = currRoles.ToList(); var rolesToRemove = roles.Except(contentItem.Groups).ToArray(); - //Depending on the action we need to first do a create or update using the membership provider - // this ensures that passwords are formatted correctly and also performs the validation on the provider itself. + //Depending on the action we need to first do a create or update using the membership manager + //this ensures that passwords are formatted correctly and also performs the validation on the provider itself. switch (contentItem.Action) { case ContentSaveAction.Save: UpdateMemberData(contentItem); break; case ContentSaveAction.SaveNew: - contentItem.PersistedContent = await CreateMemberData(contentItem); + await CreateMemberAsync(contentItem, identityMember); break; default: //we don't support anything else for members @@ -262,9 +303,6 @@ namespace Umbraco.Web.BackOffice.Controllers //TODO: There's 3 things saved here and we should do this all in one transaction, which we can do here by wrapping in a scope // but it would be nicer to have this taken care of within the Save method itself - //TODO: create/save the IMember: this is now saved in CreateAsync, remove once logic is clarified - //_memberService.Save(contentItem.PersistedContent); - //Now let's do the role provider stuff - now that we've saved the content item (that is important since // if we are changing the username, it must be persisted before looking up the member roles). if (rolesToRemove.Any()) @@ -300,13 +338,46 @@ namespace Umbraco.Web.BackOffice.Controllers return display; } + /// + /// Create the member + /// + /// + /// + /// + + private async Task CreateMemberAsync(MemberSave contentItem, UmbracoMembersIdentityUser identityMember) + { + IdentityResult created = await _memberManager.CreateAsync(identityMember, contentItem.Password.NewPassword); + if (created.Succeeded == false) + { + throw HttpResponseException.CreateNotificationValidationErrorResponse(created.Errors.ToErrorMessage()); + } + + //now re-look the member back up which will now exist + IMember member = _memberService.GetByEmail(contentItem.Email); + + member.CreatorId = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id; + member.RawPasswordValue = identityMember.RawPasswordValue; + + //since the back office user is creating this member, they will be set to approved + member.IsApproved = true; + + //map the save info over onto the user + member = _umbracoMapper.Map(contentItem, member); + contentItem.PersistedContent = member; + } + /// /// Maps the property values to the persisted entity /// /// private void MapPropertyValues(MemberSave contentItem) { - UpdateName(contentItem); + //Don't update the name if it is empty + if (contentItem.Name.IsNullOrWhiteSpace() == false) + { + contentItem.PersistedContent.Name = contentItem.Name; + } //map the custom properties - this will already be set for new entities in our member binder contentItem.PersistedContent.Email = contentItem.Email; @@ -321,83 +392,9 @@ namespace Umbraco.Web.BackOffice.Controllers null); // member are all invariant } - /// /// Create a member from the supplied member content data /// All member password processing and creation is done via the aspnet identity MemberUserManager - /// - /// - /// - private async Task CreateMemberData(MemberSave memberSave) - { - if (memberSave == null) throw new ArgumentNullException("memberSave"); - if (ModelState.IsValid == false) - { - throw new HttpResponseException(HttpStatusCode.BadRequest, ModelState); - } - //TODO: check if unique - - IMemberType memberType = _memberTypeService.Get(memberSave.ContentTypeAlias); - if (memberType == null) - { - throw new InvalidOperationException($"No member type found with alias {memberSave.ContentTypeAlias}"); - } - - // Create the member with the UserManager - // The 'empty' (special) password format is applied without us having to duplicate that logic - UmbracoMembersIdentityUser identityMember = UmbracoMembersIdentityUser.CreateNew( - memberSave.Username, - memberSave.Email, - memberSave.Name); - - //TODO: confirm - identityMember.MemberTypeAlias = memberType.Alias; - - IdentityResult created = await _memberManager.CreateAsync(identityMember); - if (created.Succeeded == false) - { - throw HttpResponseException.CreateNotificationValidationErrorResponse(created.Errors.ToErrorMessage()); - } - - //string resetPassword; - //string password = _memberManager.GeneratePassword(); - - //IdentityResult result = await _memberManager.AddPasswordAsync(identityMember, password); - //if (result.Succeeded == false) - //{ - // throw HttpResponseException.CreateNotificationValidationErrorResponse(created.Errors.ToErrorMessage()); - //} - - //resetPassword = password; - - //now re-look the member back up which will now exist - IMember member = _memberService.GetByEmail(memberSave.Email); - - //TODO: previous implementation - //IMember member = new Member( - // memberSave.Name, - // memberSave.Email, - // memberSave.Username, - // memberType, - // true) - //{ - // CreatorId = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id, - // RawPasswordValue = _memberManager.GeneratePassword(), - // Comments = memberSave.Comments, - // IsApproved = memberSave.IsApproved - //}; - - - //since the back office user is creating this member, they will be set to approved - member.IsApproved = true; - member.CreatorId = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id; - member.Comments = memberSave.Comments; - member.IsApproved = memberSave.IsApproved; - - //map the save info over onto the user - member = _umbracoMapper.Map(memberSave, member); - return member; - } /// /// Update the member security data @@ -406,7 +403,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// If the password has been reset then this method will return the reset/generated password, otherwise will return null. /// - private void UpdateMemberData(MemberSave memberSave) + private async void UpdateMemberData(MemberSave memberSave) { //TODO: optimise based on new member manager memberSave.PersistedContent.WriterId = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id; @@ -452,66 +449,12 @@ namespace Umbraco.Web.BackOffice.Controllers //no password changes then exit ? if (memberSave.Password == null) return; + //TODO: update member password functionality in manager// set the password // set the password memberSave.PersistedContent.RawPasswordValue = _memberManager.GeneratePassword(); } - private static void UpdateName(MemberSave memberSave) - { - //Don't update the name if it is empty - if (memberSave.Name.IsNullOrWhiteSpace() == false) - { - memberSave.PersistedContent.Name = memberSave.Name; - } - } - - // TODO: This logic should be pulled into the service layer - private async Task ValidateMemberDataAsync(MemberSave contentItem) - { - if (contentItem.Name.IsNullOrWhiteSpace()) - { - ModelState.AddPropertyError( - new ValidationResult("Invalid user name", new[] { "value" }), - $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login"); - return false; - } - - if (contentItem.Password != null && !contentItem.Password.NewPassword.IsNullOrWhiteSpace()) - { - //TODO: implement as per backoffice user - //var validPassword = await _memberManager.CheckPasswordAsync(null, contentItem.Password.NewPassword); - //if (!validPassword) - //{ - // ModelState.AddPropertyError( - // new ValidationResult("Invalid password: TODO", new[] { "value" }), - // $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password"); - // return false; - //} - return true; - } - - var byUsername = _memberService.GetByUsername(contentItem.Username); - if (byUsername != null && byUsername.Key != contentItem.Key) - { - ModelState.AddPropertyError( - new ValidationResult("Username is already in use", new[] { "value" }), - $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login"); - return false; - } - - var byEmail = _memberService.GetByEmail(contentItem.Email); - if (byEmail != null && byEmail.Key != contentItem.Key) - { - ModelState.AddPropertyError( - new ValidationResult("Email address is already in use", new[] { "value" }), - $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email"); - return false; - } - - return true; - } - /// /// Permanently deletes a member /// From cf2252f9672f4f69f1a52b303adc48f8348a7df4 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Thu, 3 Dec 2020 14:59:57 +0000 Subject: [PATCH 14/72] Reordered for better comparison --- .../Members/UmbracoMembersUserManager.cs | 5 +- .../Search/ExamineComponent.cs | 1 + .../Controllers/MemberController.cs | 284 ++++++++++-------- 3 files changed, 154 insertions(+), 136 deletions(-) diff --git a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserManager.cs b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserManager.cs index ce6e20210e..6f227b7a2c 100644 --- a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserManager.cs +++ b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserManager.cs @@ -3,15 +3,12 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Umbraco.Core.Configuration; using Umbraco.Core.Members; using Umbraco.Core.Security; using System.Threading; -using Umbraco.Core; using Umbraco.Core.Configuration.Models; -using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Infrastructure.Members @@ -107,7 +104,7 @@ namespace Umbraco.Infrastructure.Members return IdentityResult.Success; } - ///TODO: duplicated code from backofficeusermanager + ///TODO: duplicated code from backofficeusermanager, could be shared? /// /// Logic used to validate a username and password /// diff --git a/src/Umbraco.Infrastructure/Search/ExamineComponent.cs b/src/Umbraco.Infrastructure/Search/ExamineComponent.cs index e866fac560..9dda6865d3 100644 --- a/src/Umbraco.Infrastructure/Search/ExamineComponent.cs +++ b/src/Umbraco.Infrastructure/Search/ExamineComponent.cs @@ -515,6 +515,7 @@ namespace Umbraco.Web.Search DeferedReIndexForContent.Execute(this, sender, isPublished); } + //TODO: this causes an exception when deleting a member private void ReIndexForMember(IMember member) { var actions = DeferedActions.Get(_scopeProvider); diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index a6d440d505..13954a1810 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -17,7 +17,6 @@ using Umbraco.Core.Events; using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Models.ContentEditing; -using Umbraco.Core.Models.Membership; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Security; using Umbraco.Core.Serialization; @@ -193,9 +192,9 @@ namespace Umbraco.Web.BackOffice.Controllers throw new HttpResponseException(HttpStatusCode.NotFound); } - IMember emptyContent = new Member(contentType); - string newPassword = _memberManager.GeneratePassword(); + + IMember emptyContent = new Member(contentType); emptyContent.AdditionalData["NewPassword"] = newPassword; return _umbracoMapper.Map(emptyContent); } @@ -228,6 +227,154 @@ namespace Umbraco.Web.BackOffice.Controllers //map the properties to the persisted entity MapPropertyValues(contentItem); + UmbracoMembersIdentityUser identityMember = ValidateMemberData(contentItem); + + //Unlike content/media - if there are errors for a member, we do NOT proceed to save them, we cannot so return the errors + if (ModelState.IsValid == false) + { + var forDisplay = _umbracoMapper.Map(contentItem.PersistedContent); + forDisplay.Errors = ModelState.ToErrorDictionary(); + throw HttpResponseException.CreateValidationErrorResponse(forDisplay); + } + + //We're gonna look up the current roles now because the below code can cause + // events to be raised and developers could be manually adding roles to members in + // their handlers. If we don't look this up now there's a chance we'll just end up + // removing the roles they've assigned. + var currRoles = _memberService.GetAllRoles(contentItem.PersistedContent.Username); + + //find the ones to remove and remove them + IEnumerable roles = currRoles.ToList(); + var rolesToRemove = roles.Except(contentItem.Groups).ToArray(); + + //Depending on the action we need to first do a create or update using the membership manager + //this ensures that passwords are formatted correctly and also performs the validation on the provider itself. + switch (contentItem.Action) + { + case ContentSaveAction.Save: + UpdateMemberData(contentItem); + break; + case ContentSaveAction.SaveNew: + await CreateMemberAsync(contentItem, identityMember); + break; + default: + //we don't support anything else for members + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + //TODO: There's 3 things saved here and we should do this all in one transaction, which we can do here by wrapping in a scope + // but it would be nicer to have this taken care of within the Save method itself + + //Now let's do the role provider stuff - now that we've saved the content item (that is important since + // if we are changing the username, it must be persisted before looking up the member roles). + if (rolesToRemove.Any()) + { + _memberService.DissociateRoles(new[] { contentItem.PersistedContent.Username }, rolesToRemove); + } + //find the ones to add and add them + var toAdd = contentItem.Groups.Except(roles).ToArray(); + if (toAdd.Any()) + { + //add the ones submitted + _memberService.AssignRoles(new[] { contentItem.PersistedContent.Username }, toAdd); + } + + //return the updated model + var display = _umbracoMapper.Map(contentItem.PersistedContent); + + //lastly, if it is not valid, add the model state to the outgoing object and throw a 403 + HandleInvalidModelState(display); + + var localizedTextService = _localizedTextService; + //put the correct messages in + switch (contentItem.Action) + { + case ContentSaveAction.Save: + case ContentSaveAction.SaveNew: + display.AddSuccessNotification( + localizedTextService.Localize("speechBubbles/editMemberSaved"), + localizedTextService.Localize("speechBubbles/editMemberSaved")); + break; + } + + return display; + } + + /// + /// Maps the property values to the persisted entity + /// + /// + private void MapPropertyValues(MemberSave contentItem) + { + //Don't update the name if it is empty + if (contentItem.Name.IsNullOrWhiteSpace() == false) + { + contentItem.PersistedContent.Name = contentItem.Name; + } + + //map the custom properties - this will already be set for new entities in our member binder + contentItem.PersistedContent.Email = contentItem.Email; + contentItem.PersistedContent.Username = contentItem.Username; + + //use the base method to map the rest of the properties + base.MapPropertyValuesForPersistence( + contentItem, + contentItem.PropertyCollectionDto, + (save, property) => property.GetValue(), //get prop val + (save, property, v) => property.SetValue(v), //set prop val + null); // member are all invariant + } + + /// + /// Create a member from the supplied member content data + /// All member password processing and creation is done via the aspnet identity MemberUserManager + /// + /// + /// + /// + private async Task CreateMemberAsync(MemberSave contentItem, UmbracoMembersIdentityUser identityMember) + { + // TODO: all member password processing and creation needs to be done with a new aspnet identity MemberUserManager that hasn't been created yet. + + //var memberType = _memberTypeService.Get(contentItem.ContentTypeAlias); + //if (memberType == null) + // throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}"); + //var member = new Member(contentItem.Name, contentItem.Email, contentItem.Username, memberType, true) + //{ + // CreatorId = _backofficeSecurityAccessor.BackofficeSecurity.CurrentUser.Id, + // RawPasswordValue = _passwordSecurity.HashPasswordForStorage(contentItem.Password.NewPassword), + // Comments = contentItem.Comments, + // IsApproved = contentItem.IsApproved + //}; + + //return member; + + + + + IdentityResult created = await _memberManager.CreateAsync(identityMember, contentItem.Password.NewPassword); + if (created.Succeeded == false) + { + throw HttpResponseException.CreateNotificationValidationErrorResponse(created.Errors.ToErrorMessage()); + } + + //now re-look the member back up which will now exist + IMember member = _memberService.GetByEmail(contentItem.Email); + + member.CreatorId = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id; + member.RawPasswordValue = identityMember.RawPasswordValue; + + //since the back office user is creating this member, they will be set to approved + member.IsApproved = true; + + //map the save info over onto the user + member = _umbracoMapper.Map(contentItem, member); + contentItem.PersistedContent = member; + } + + private UmbracoMembersIdentityUser ValidateMemberData(MemberSave contentItem) + { + var memberType = _memberTypeService.Get(contentItem.ContentTypeAlias); if (memberType == null) { @@ -266,136 +413,9 @@ namespace Umbraco.Web.BackOffice.Controllers //TODO: confirm where to do this identityMember.RawPasswordValue = contentItem.Password.NewPassword; - - //Unlike content/media - if there are errors for a member, we do NOT proceed to save them, we cannot so return the errors - if (ModelState.IsValid == false) - { - var forDisplay = _umbracoMapper.Map(contentItem.PersistedContent); - forDisplay.Errors = ModelState.ToErrorDictionary(); - throw HttpResponseException.CreateValidationErrorResponse(forDisplay); - } - - //We're gonna look up the current roles now because the below code can cause - // events to be raised and developers could be manually adding roles to members in - // their handlers. If we don't look this up now there's a chance we'll just end up - // removing the roles they've assigned. - IEnumerable currRoles = _memberService.GetAllRoles(contentItem.PersistedContent.Username); - - //find the ones to remove and remove them - IEnumerable roles = currRoles.ToList(); - var rolesToRemove = roles.Except(contentItem.Groups).ToArray(); - - //Depending on the action we need to first do a create or update using the membership manager - //this ensures that passwords are formatted correctly and also performs the validation on the provider itself. - switch (contentItem.Action) - { - case ContentSaveAction.Save: - UpdateMemberData(contentItem); - break; - case ContentSaveAction.SaveNew: - await CreateMemberAsync(contentItem, identityMember); - break; - default: - //we don't support anything else for members - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - //TODO: There's 3 things saved here and we should do this all in one transaction, which we can do here by wrapping in a scope - // but it would be nicer to have this taken care of within the Save method itself - - //Now let's do the role provider stuff - now that we've saved the content item (that is important since - // if we are changing the username, it must be persisted before looking up the member roles). - if (rolesToRemove.Any()) - { - _memberService.DissociateRoles(new[] { contentItem.PersistedContent.Username }, rolesToRemove); - } - //find the ones to add and add them - string[] toAdd = contentItem.Groups.Except(roles).ToArray(); - if (toAdd.Any()) - { - //add the ones submitted - _memberService.AssignRoles(new[] { contentItem.PersistedContent.Username }, toAdd); - } - - //return the updated model - MemberDisplay display = _umbracoMapper.Map(contentItem.PersistedContent); - - //lastly, if it is not valid, add the model state to the outgoing object and throw a 403 - HandleInvalidModelState(display); - - ILocalizedTextService localizedTextService = _localizedTextService; - //put the correct messages in - switch (contentItem.Action) - { - case ContentSaveAction.Save: - case ContentSaveAction.SaveNew: - display.AddSuccessNotification( - localizedTextService.Localize("speechBubbles/editMemberSaved"), - localizedTextService.Localize("speechBubbles/editMemberSaved")); - break; - } - - return display; + return identityMember; } - /// - /// Create the member - /// - /// - /// - /// - - private async Task CreateMemberAsync(MemberSave contentItem, UmbracoMembersIdentityUser identityMember) - { - IdentityResult created = await _memberManager.CreateAsync(identityMember, contentItem.Password.NewPassword); - if (created.Succeeded == false) - { - throw HttpResponseException.CreateNotificationValidationErrorResponse(created.Errors.ToErrorMessage()); - } - - //now re-look the member back up which will now exist - IMember member = _memberService.GetByEmail(contentItem.Email); - - member.CreatorId = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id; - member.RawPasswordValue = identityMember.RawPasswordValue; - - //since the back office user is creating this member, they will be set to approved - member.IsApproved = true; - - //map the save info over onto the user - member = _umbracoMapper.Map(contentItem, member); - contentItem.PersistedContent = member; - } - - /// - /// Maps the property values to the persisted entity - /// - /// - private void MapPropertyValues(MemberSave contentItem) - { - //Don't update the name if it is empty - if (contentItem.Name.IsNullOrWhiteSpace() == false) - { - contentItem.PersistedContent.Name = contentItem.Name; - } - - //map the custom properties - this will already be set for new entities in our member binder - contentItem.PersistedContent.Email = contentItem.Email; - contentItem.PersistedContent.Username = contentItem.Username; - - //use the base method to map the rest of the properties - base.MapPropertyValuesForPersistence( - contentItem, - contentItem.PropertyCollectionDto, - (save, property) => property.GetValue(), //get prop val - (save, property, v) => property.SetValue(v), //set prop val - null); // member are all invariant - } - - /// Create a member from the supplied member content data - /// All member password processing and creation is done via the aspnet identity MemberUserManager - - /// /// Update the member security data /// @@ -489,7 +509,7 @@ namespace Umbraco.Web.BackOffice.Controllers return Forbid(); } - MemberExportModel member = ((MemberService)_memberService).ExportMember(key); + var member = ((MemberService)_memberService).ExportMember(key); if (member is null) throw new NullReferenceException("No member found with key " + key); var json = _jsonSerializer.Serialize(member); From 785570761aba2d0b39bd159b5ccc7ebd8fe3c443 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Thu, 3 Dec 2020 15:59:54 +0000 Subject: [PATCH 15/72] Removed identity work for now --- .../BackOffice/IdentityMapDefinition.cs | 2 +- .../Members/UmbracoMembersIdentityUser.cs | 8 -------- .../Members/UmbracoMembersUserManager.cs | 2 +- .../Members/UmbracoMembersUserStore.cs | 16 ++++++++-------- 4 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Core/BackOffice/IdentityMapDefinition.cs b/src/Umbraco.Core/BackOffice/IdentityMapDefinition.cs index caa8f41ea5..b2702c1ddb 100644 --- a/src/Umbraco.Core/BackOffice/IdentityMapDefinition.cs +++ b/src/Umbraco.Core/BackOffice/IdentityMapDefinition.cs @@ -95,7 +95,7 @@ namespace Umbraco.Core.BackOffice { target.Email = source.Email; target.UserName = source.Username; - target.LastPasswordChangeDateUtc = source.LastPasswordChangeDate.ToUniversalTime(); + //target.LastPasswordChangeDateUtc = source.LastPasswordChangeDate.ToUniversalTime(); //target.LastLoginDateUtc = source.LastLoginDate.ToUniversalTime(); //target.EmailConfirmed = source.EmailConfirmedDate.HasValue; target.Name = source.Name; diff --git a/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs b/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs index 89ede9d4a6..da4f823c70 100644 --- a/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs +++ b/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs @@ -1,6 +1,4 @@ using System; -using Umbraco.Core.Models.Entities; -using Umbraco.Core.Models.Identity; namespace Umbraco.Core.Members { @@ -21,7 +19,6 @@ namespace Umbraco.Core.Members public string MemberTypeAlias { get; set; } public bool IsLockedOut { get; set; } public string RawPasswordValue { get; set; } - public DateTime LastPasswordChangeDateUtc { get; set; } /// /// Returns true if an Id has been set on this object @@ -45,12 +42,7 @@ namespace Umbraco.Core.Members //TODO: config public string PasswordConfig { get; set; } - string Comment; internal bool IsApproved; - DateTime LastLockoutDate; - DateTime CreationDate; - DateTime LastLoginDate; - DateTime LastActivityDate; //TODO: needed? //public bool LoginsChanged; diff --git a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserManager.cs b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserManager.cs index 6f227b7a2c..1637eb2ff7 100644 --- a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserManager.cs +++ b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserManager.cs @@ -84,7 +84,7 @@ namespace Umbraco.Infrastructure.Members /// protected override async Task UpdatePasswordHash(T memberUser, string newPassword, bool validatePassword) { - memberUser.LastPasswordChangeDateUtc = DateTime.UtcNow; + //memberUser.LastPasswordChangeDateUtc = DateTime.UtcNow; if (validatePassword) { diff --git a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs index ba7d5d9324..e4db2b34cb 100644 --- a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs +++ b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs @@ -16,14 +16,14 @@ namespace Umbraco.Infrastructure.Members /// public class UmbracoMembersUserStore : DisposableObjectSlim, IUserStore, - IUserPasswordStore - //IUserEmailStore - //IUserLoginStore - //IUserRoleStore, - //IUserSecurityStampStore - //IUserLockoutStore - //IUserTwoFactorStore - //IUserSessionStore + IUserPasswordStore + //IUserEmailStore + //IUserLoginStore + //IUserRoleStore, + //IUserSecurityStampStore + //IUserLockoutStore + //IUserTwoFactorStore + //IUserSessionStore { private bool _disposed = false; private readonly IMemberService _memberService; From 57684a8672429eabc6e7ded089820bfe12a59957 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Thu, 3 Dec 2020 17:54:02 +0000 Subject: [PATCH 16/72] Removed unwanted comments --- .../Members/UmbracoMembersUserStore.cs | 50 ++----------------- .../Controllers/MemberController.cs | 26 +++++++--- .../Security/MembershipProviderBase.cs | 1 + 3 files changed, 22 insertions(+), 55 deletions(-) diff --git a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs index e4db2b34cb..0d8b296c05 100644 --- a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs +++ b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs @@ -15,7 +15,7 @@ namespace Umbraco.Infrastructure.Members /// A custom user store that uses Umbraco member data /// public class UmbracoMembersUserStore : DisposableObjectSlim, - IUserStore, + //IUserStore, IUserPasswordStore //IUserEmailStore //IUserLoginStore @@ -52,8 +52,7 @@ namespace Umbraco.Infrastructure.Members UpdateMemberProperties(member, user); - //TODO: do we want to accept empty passwords here - if thirdparty for example? In other method if so? - + //TODO: do we want to accept empty passwords here - if third-party for example? In other method if so? _memberService.Save(member); //re-assign id @@ -106,33 +105,6 @@ namespace Umbraco.Infrastructure.Members //[Comments as per BackOfficeUserStore & identity package] var anythingChanged = false; //don't assign anything if nothing has changed as this will trigger the track changes of the model - - //if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.LastLoginDateUtc)) - // || (member.LastLoginDate != default(DateTime) && identityUser.LastLoginDateUtc.HasValue == false) - // || identityUser.LastLoginDateUtc.HasValue && member.LastLoginDate.ToUniversalTime() != identityUser.LastLoginDateUtc.Value) - //{ - // anythingChanged = true; - // //if the LastLoginDate is being set to MinValue, don't convert it ToLocalTime - // var dt = identityUser.LastLoginDateUtc == DateTime.MinValue ? DateTime.MinValue : identityUser.LastLoginDateUtc.Value.ToLocalTime(); - // member.LastLoginDate = dt; - //} - - //if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.LastPasswordChangeDateUtc)) - // || (member.LastPasswordChangeDate != default(DateTime) && identityUser.LastPasswordChangeDateUtc.HasValue == false) - // || identityUser.LastPasswordChangeDateUtc.HasValue && member.LastPasswordChangeDate.ToUniversalTime() != identityUser.LastPasswordChangeDateUtc.Value) - //{ - // anythingChanged = true; - // member.LastPasswordChangeDate = identityUser.LastPasswordChangeDateUtc.Value.ToLocalTime(); - //} - - //if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.EmailConfirmed)) - // || (member.EmailConfirmedDate.HasValue && member.EmailConfirmedDate.Value != default(DateTime) && identityUser.EmailConfirmed == false) - // || ((member.EmailConfirmedDate.HasValue == false || member.EmailConfirmedDate.Value == default(DateTime)) && identityUser.EmailConfirmed)) - //{ - // anythingChanged = true; - // member.EmailConfirmedDate = identityUser.EmailConfirmed ? (DateTime?)DateTime.Now : null; - //} - if ( //memberIdentityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Name)) && member.Name != memberIdentityUser.Name && memberIdentityUser.Name.IsNullOrWhiteSpace() == false) @@ -147,15 +119,7 @@ namespace Umbraco.Infrastructure.Members anythingChanged = true; member.Email = memberIdentityUser.Email; } - - //TODO: AccessFailedCount - //if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.AccessFailedCount)) - // && member.FailedPasswordAttempts != identityUser.AccessFailedCount) - //{ - // anythingChanged = true; - // member.FailedPasswordAttempts = identityUser.AccessFailedCount; - //} - + if (member.IsLockedOut != memberIdentityUser.IsLockedOut) { anythingChanged = true; @@ -175,7 +139,6 @@ namespace Umbraco.Infrastructure.Members member.Username = memberIdentityUser.UserName; } - //TODO: PasswordHash and PasswordConfig if ( //member.IsPropertyDirty(nameof(BackOfficeIdentityUser.PasswordHash))&& member.RawPasswordValue != memberIdentityUser.PasswordHash @@ -186,13 +149,6 @@ namespace Umbraco.Infrastructure.Members member.PasswordConfiguration = memberIdentityUser.PasswordConfig; } - //TODO: SecurityStamp - //if (member.SecurityStamp != identityUser.SecurityStamp) - //{ - // anythingChanged = true; - // member.SecurityStamp = identityUser.SecurityStamp; - //} - // TODO: Roles // [Comment] Same comment as per BackOfficeUserStore: Fix this for Groups too //if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Roles)) || identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Groups))) diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index 13954a1810..f16667a779 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -334,8 +334,6 @@ namespace Umbraco.Web.BackOffice.Controllers /// private async Task CreateMemberAsync(MemberSave contentItem, UmbracoMembersIdentityUser identityMember) { - // TODO: all member password processing and creation needs to be done with a new aspnet identity MemberUserManager that hasn't been created yet. - //var memberType = _memberTypeService.Get(contentItem.ContentTypeAlias); //if (memberType == null) // throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}"); @@ -349,9 +347,6 @@ namespace Umbraco.Web.BackOffice.Controllers //return member; - - - IdentityResult created = await _memberManager.CreateAsync(identityMember, contentItem.Password.NewPassword); if (created.Succeeded == false) { @@ -362,7 +357,6 @@ namespace Umbraco.Web.BackOffice.Controllers IMember member = _memberService.GetByEmail(contentItem.Email); member.CreatorId = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id; - member.RawPasswordValue = identityMember.RawPasswordValue; //since the back office user is creating this member, they will be set to approved member.IsApproved = true; @@ -374,7 +368,6 @@ namespace Umbraco.Web.BackOffice.Controllers private UmbracoMembersIdentityUser ValidateMemberData(MemberSave contentItem) { - var memberType = _memberTypeService.Get(contentItem.ContentTypeAlias); if (memberType == null) { @@ -404,13 +397,30 @@ namespace Umbraco.Web.BackOffice.Controllers $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email"); } + if (contentItem.Password != null && !contentItem.Password.NewPassword.IsNullOrWhiteSpace()) + { + //TODO: check password + //var validPassword = await _memberManager.CheckPasswordAsync(null, contentItem.Password.NewPassword); + //if (!validPassword) + //{ + // ModelState.AddPropertyError( + // new ValidationResult("Invalid password", new[] { "value" }), + // $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password"); + //} + } + else + { + ModelState.AddPropertyError( + new ValidationResult("Password cannot be empty", new[] { "value" }), + $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password"); + } + // Create the member with the MemberManager var identityMember = UmbracoMembersIdentityUser.CreateNew( contentItem.Username, contentItem.Email, memberType.Alias, contentItem.Name); - //TODO: confirm where to do this identityMember.RawPasswordValue = contentItem.Password.NewPassword; return identityMember; diff --git a/src/Umbraco.Web/Security/MembershipProviderBase.cs b/src/Umbraco.Web/Security/MembershipProviderBase.cs index a62ef958c4..669b105775 100644 --- a/src/Umbraco.Web/Security/MembershipProviderBase.cs +++ b/src/Umbraco.Web/Security/MembershipProviderBase.cs @@ -19,6 +19,7 @@ namespace Umbraco.Web.Security /// /// A base membership provider class offering much of the underlying functionality for initializing and password encryption/hashing. /// + [Obsolete("Will be replaced by UmbracoMemberUserManager")] public abstract class MembershipProviderBase : MembershipProvider { private readonly IHostingEnvironment _hostingEnvironment; From 40f2a881ab5628bfa42790e8177032fc24d69e31 Mon Sep 17 00:00:00 2001 From: emmagarland Date: Sat, 5 Dec 2020 23:44:50 +0000 Subject: [PATCH 17/72] Added is dirty properties and updated to reflect linter update --- .../Members/UmbracoMembersIdentityUser.cs | 162 ++++++++-- .../Members/IUmbracoMembersUserManager.cs | 17 +- .../Members/UmbracoMembersUserManager.cs | 149 +++++---- .../Members/UmbracoMembersUserStore.cs | 228 ++++++++++---- .../Services/Implement/MemberService.cs | 19 +- .../UmbracoMemberIdentityUserStoreTests.cs | 6 +- .../Controllers/ContentControllerBase.cs | 93 +++--- .../Controllers/MemberController.cs | 285 ++++++++++-------- src/umbraco.sln.DotSettings | 1 + 9 files changed, 644 insertions(+), 316 deletions(-) diff --git a/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs b/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs index da4f823c70..fb92edd47d 100644 --- a/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs +++ b/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs @@ -1,77 +1,179 @@ -using System; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using Umbraco.Core.Models.Entities; namespace Umbraco.Core.Members { /// /// An Umbraco member user type + /// TODO: use of identity classes in future /// - public class UmbracoMembersIdentityUser - //: IRememberBeingDirty - //TODO: use of identity classes - //: IdentityUser, IdentityUserClaim>, + public class UmbracoMembersIdentityUser : IRememberBeingDirty { - private bool _hasIdentity; private int _id; - public string Name { get; set; } - public string Email { get; set; } - public string UserName { get; set; } - public string MemberTypeAlias { get; set; } - public bool IsLockedOut { get; set; } - public string RawPasswordValue { get; set; } + private string _passwordHash; + + private string _passwordConfig; /// - /// Returns true if an Id has been set on this object + /// Gets or sets the member name + /// + public string Name { get; set; } + + /// + /// Gets or sets the member email + /// + public string Email { get; set; } + + /// + /// Gets or sets the member username + /// + public string UserName { get; set; } + + /// + /// Gets or sets the alias of the member type + /// + public string MemberTypeAlias { get; set; } + + /// + /// Gets or sets a value indicating whether the member is locked out + /// + public bool IsLockedOut { get; set; } + + /// + /// Gets a value indicating whether an Id has been set on this object /// This will be false if the object is new and not persisted to the database /// - public bool HasIdentity => _hasIdentity; + public bool HasIdentity { get; private set; } + /// + /// Gets or sets the member Id + /// public int Id { get => _id; set { _id = value; - _hasIdentity = true; + HasIdentity = true; } } - //TODO: track - public string PasswordHash { get; set; } + /// + /// Gets or sets the salted/hashed form of the user password + /// + public string PasswordHash + { + get => _passwordHash; + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordHash, nameof(PasswordHash)); + } - //TODO: config - public string PasswordConfig { get; set; } + /// + /// Gets or sets the password config + /// + public string PasswordConfig + { + get => _passwordConfig; + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfig)); + } - internal bool IsApproved; + /// + /// Gets or sets a value indicating whether member Is Approved + /// + public bool IsApproved { get; set; } - //TODO: needed? + /// + /// Gets the for change tracking + /// + protected BeingDirty BeingDirty { get; } = new BeingDirty(); + + // TODO: implement as per base identity user //public bool LoginsChanged; //public bool RolesChanged; - + /// + /// Create a new identity member + /// + /// The member username + /// The member email + /// The member type alias + /// The member name + /// TODO: confirm The password (may be null in some instances) + /// Throws is username is null or whitespace + /// The identity member user public static UmbracoMembersIdentityUser CreateNew( string username, string email, string memberTypeAlias, - string name) + string name, + string password = null) { - if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username)); + if (string.IsNullOrWhiteSpace(username)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(username)); + } - //no groups/roles yet + // no groups/roles yet var member = new UmbracoMembersIdentityUser { UserName = username, Email = email, Name = name, MemberTypeAlias = memberTypeAlias, - Id = 0, //TODO: is this meant to be 0 in this circumstance? - //false by default unless specifically set - _hasIdentity = false + Id = 0, // TODO: is this meant to be 0 in this circumstance? + // false by default unless specifically set + HasIdentity = false }; - //TODO: do we use this? - //member.EnableChangeTracking(); + member.EnableChangeTracking(); return member; } + + /// + public event PropertyChangedEventHandler PropertyChanged + { + add => BeingDirty.PropertyChanged += value; + + remove => BeingDirty.PropertyChanged -= value; + } + + /// + + public bool IsDirty() => BeingDirty.IsDirty(); + + /// + public bool IsPropertyDirty(string propName) => BeingDirty.IsPropertyDirty(propName); + + /// + public IEnumerable GetDirtyProperties() => BeingDirty.GetDirtyProperties(); + + /// + + public void ResetDirtyProperties() => BeingDirty.ResetDirtyProperties(); + + /// + + public void DisableChangeTracking() => BeingDirty.DisableChangeTracking(); + + /// + + public void EnableChangeTracking() => BeingDirty.EnableChangeTracking(); + + /// + public bool WasDirty() => BeingDirty.WasDirty(); + + /// + public bool WasPropertyDirty(string propertyName) => BeingDirty.WasPropertyDirty(propertyName); + + /// + public void ResetWereDirtyProperties() => BeingDirty.ResetWereDirtyProperties(); + + /// + public void ResetDirtyProperties(bool rememberDirty) => BeingDirty.ResetDirtyProperties(rememberDirty); + + /// + public IEnumerable GetWereDirtyProperties() => BeingDirty.GetWereDirtyProperties(); } } diff --git a/src/Umbraco.Infrastructure/Members/IUmbracoMembersUserManager.cs b/src/Umbraco.Infrastructure/Members/IUmbracoMembersUserManager.cs index 02c1436a44..8eedb67af6 100644 --- a/src/Umbraco.Infrastructure/Members/IUmbracoMembersUserManager.cs +++ b/src/Umbraco.Infrastructure/Members/IUmbracoMembersUserManager.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Umbraco.Core.Members; @@ -12,11 +13,10 @@ namespace Umbraco.Infrastructure.Members public interface IUmbracoMembersUserManager : IDisposable where TUser : UmbracoMembersIdentityUser { /// - /// Creates the specified in the backing store with a password, - /// as an asynchronous operation. + /// Creates the specified user in the backing store with given password, as an asynchronous operation. /// /// The member to create. - /// The password to add + /// The new password /// /// The that represents the asynchronous operation, containing the /// of the operation. @@ -26,7 +26,7 @@ namespace Umbraco.Infrastructure.Members /// /// Helper method to generate a password for a user based on the current password validator /// - /// + /// Returns the generated password string GeneratePassword(); /// @@ -51,5 +51,12 @@ namespace Umbraco.Infrastructure.Members /// the specified matches the one store for the , /// otherwise false. Task CheckPasswordAsync(TUser memberUser, string password); + + /// + /// Method to validate the password without an identity user + /// + /// The password to validate + /// The result of the validation + Task> ValidatePassword(string password); } } diff --git a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserManager.cs b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserManager.cs index 1637eb2ff7..3ab230a76e 100644 --- a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserManager.cs +++ b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserManager.cs @@ -1,14 +1,14 @@ -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.Models; using Umbraco.Core.Members; using Umbraco.Core.Security; -using System.Threading; -using Umbraco.Core.Configuration.Models; namespace Umbraco.Infrastructure.Members @@ -18,6 +18,7 @@ namespace Umbraco.Infrastructure.Members /// public class UmbracoMembersUserManager : UmbracoMembersUserManager, IUmbracoMembersUserManager { + /// public UmbracoMembersUserManager( IUserStore store, IOptions optionsAccessor, @@ -34,13 +35,28 @@ namespace Umbraco.Infrastructure.Members } } + /// + /// Manager for the member identity user + /// + /// The identity user public class UmbracoMembersUserManager : UserManager where T : UmbracoMembersIdentityUser { - public IPasswordConfiguration PasswordConfiguration { get; protected set; } - private PasswordGenerator _passwordGenerator; + /// + /// Initializes a new instance of the class. + /// + /// The members store + /// The identity options accessor + /// The password hasher + /// The user validators + /// The password validators + /// The keep lookup normalizer + /// The error display messages + /// The service provider + /// The logger + /// The password configuration public UmbracoMembersUserManager( IUserStore store, IOptions optionsAccessor, @@ -51,14 +67,17 @@ namespace Umbraco.Infrastructure.Members IdentityErrorDescriber errors, IServiceProvider services, ILogger> logger, - IOptions passwordConfiguration) : - base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) - { + IOptions passwordConfiguration) : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) => PasswordConfiguration = passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration)); - } + /// - /// Replace the underlying options property with our own strongly typed version + /// Gets or sets the password configuration + /// + public IPasswordConfiguration PasswordConfiguration { get; protected set; } + + /// + /// gets or sets the underlying options property with our own strongly typed version /// public new UmbracoMembersIdentityOptions Options { @@ -67,24 +86,24 @@ namespace Umbraco.Infrastructure.Members } /// - /// Gets/sets the default Umbraco member user password checker + /// Gets or sets the default Umbraco member user password checker /// public IUmbracoMembersUserPasswordChecker UmbracoMembersUserPasswordChecker { get; set; } - /// - /// [TODO: from BackOfficeUserManager duplicated, could be shared] - /// 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) - /// + /// + /// TODO: from BackOfficeUserManager duplicated, could be shared + /// Override to determine how to hash the password + /// + /// The member to validate + /// The new password + /// Whether to validate the password + /// The identity result of updating the password hash + /// + /// 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 UpdatePasswordHash(T memberUser, string newPassword, bool validatePassword) { - //memberUser.LastPasswordChangeDateUtc = DateTime.UtcNow; + // memberUser.LastPasswordChangeDateUtc = DateTime.UtcNow; if (validatePassword) { @@ -95,8 +114,10 @@ namespace Umbraco.Infrastructure.Members } } - var passwordStore = Store as IUserPasswordStore; - if (passwordStore == null) throw new NotSupportedException("The current user store does not implement " + typeof(IUserPasswordStore<>)); + if (!(Store is IUserPasswordStore passwordStore)) + { + throw new NotSupportedException("The current user store does not implement " + typeof(IUserPasswordStore<>)); + } var hash = newPassword != null ? PasswordHasher.HashPassword(memberUser, newPassword) : null; await passwordStore.SetPasswordHashAsync(memberUser, hash, CancellationToken); @@ -104,13 +125,13 @@ namespace Umbraco.Infrastructure.Members return IdentityResult.Success; } - ///TODO: duplicated code from backofficeusermanager, could be shared? + /// TODO: duplicated code from backofficeusermanager, could be shared /// /// Logic used to validate a username and password /// - /// - /// - /// + /// The member to validate + /// The password to validate + /// Whether the password is the correct password for this member /// /// By default this uses the standard ASP.Net Identity approach which is: /// * Get password store @@ -136,68 +157,88 @@ namespace Umbraco.Infrastructure.Members return false; } - //if the result indicates to not fallback to the default, then return true if the credentials are valid + // if the result indicates to not fallback to the default, then return true if the credentials are valid if (result != UmbracoMembersUserPasswordCheckerResult.FallbackToDefaultChecker) { return result == UmbracoMembersUserPasswordCheckerResult.ValidCredentials; } } - //we cannot proceed if the user passed in does not have an identity + // we cannot proceed if the user passed in does not have an identity if (member.HasIdentity == false) + { return false; + } - //use the default behavior + // use the default behavior return await base.CheckPasswordAsync(member, password); } - ///[TODO: from BackOfficeUserManager duplicated, could be shared] + /// TODO: from BackOfficeUserManager duplicated, could be shared /// /// This is copied from the underlying .NET base class since they decided to not expose it /// - /// - /// + /// The user to update the security stamp for + /// Task returns private async Task UpdateSecurityStampInternal(T user) { - if (SupportsUserSecurityStamp == false) return; + if (SupportsUserSecurityStamp == false) + { + return; + } + await GetSecurityStore().SetSecurityStampAsync(user, NewSecurityStamp(), CancellationToken.None); } - ///[TODO: from BackOfficeUserManager duplicated, could be shared] + /// TODO: from BackOfficeUserManager duplicated, could be shared /// /// This is copied from the underlying .NET base class since they decided to not expose it /// - /// + /// Return a user security stamp private IUserSecurityStampStore GetSecurityStore() { - var store = Store as IUserSecurityStampStore; - if (store == null) throw new NotSupportedException("The current user store does not implement " + typeof(IUserSecurityStampStore<>)); + if (!(Store is IUserSecurityStampStore store)) + { + throw new NotSupportedException("The current user store does not implement " + typeof(IUserSecurityStampStore<>)); + } + return store; } - ///[TODO: from BackOfficeUserManager duplicated, could be shared] + /// TODO: from BackOfficeUserManager duplicated, could be shared /// /// This is copied from the underlying .NET base class since they decided to not expose it /// - /// - private static string NewSecurityStamp() - { - return Guid.NewGuid().ToString(); - } + /// Returns a new security stamp + private static string NewSecurityStamp() => Guid.NewGuid().ToString(); - ///[TODO: from BackOfficeUserManager duplicated, could be shared] /// + /// TODO: from BackOfficeUserManager duplicated, could be shared /// Helper method to generate a password for a member based on the current password validator /// - /// + /// The generated password public string GeneratePassword() { - if (_passwordGenerator == null) - { - _passwordGenerator = new PasswordGenerator(PasswordConfiguration); - } + _passwordGenerator ??= new PasswordGenerator(PasswordConfiguration); string password = _passwordGenerator.GeneratePassword(); return password; } + + /// + /// Helper method to validate a password based on the current password validator + /// + /// The password to update + /// The validated password + public async Task> ValidatePassword(string password) + { + var passwordValidators = new List(); + foreach(IPasswordValidator validator in PasswordValidators) + { + IdentityResult result = await validator.ValidateAsync(this, null, password); + passwordValidators.Add(result); + } + + return passwordValidators; + } } } diff --git a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs index 0d8b296c05..d68b21dc1b 100644 --- a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs +++ b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs @@ -1,12 +1,15 @@ -using Microsoft.AspNetCore.Identity; using System; using System.Data; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; using Umbraco.Core; +using Umbraco.Core.BackOffice; using Umbraco.Core.Mapping; using Umbraco.Core.Members; using Umbraco.Core.Models; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; namespace Umbraco.Infrastructure.Members @@ -25,40 +28,57 @@ namespace Umbraco.Infrastructure.Members //IUserTwoFactorStore //IUserSessionStore { - private bool _disposed = false; + private readonly bool _disposed = false; private readonly IMemberService _memberService; private readonly UmbracoMapper _mapper; + private readonly IScopeProvider _scopeProvider; - public UmbracoMembersUserStore(IMemberService memberService, UmbracoMapper mapper) + /// + /// Initializes a new instance of the class for the members identity store + /// + /// The member service + /// The mapper for properties + /// The scope provider + public UmbracoMembersUserStore(IMemberService memberService, UmbracoMapper mapper, IScopeProvider scopeProvider) { _memberService = memberService; _mapper = mapper; + _scopeProvider = scopeProvider; } + /// + /// Create the member as an identity user + /// + /// The identity user` for a member + /// The cancellation token + /// The identity result public Task CreateAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } - //create member - //TODO: are we keeping this method, e.g. the Member Service? The user service creates it directly, but this gets the membertype + // create member + // TODO: are we keeping this method, e.g. the Member Service? + // The user service creates it directly, but this way we get the member type by alias first IMember member = _memberService.CreateMember( user.UserName, user.Email, user.Name.IsNullOrWhiteSpace() ? user.UserName : user.Name, - user.MemberTypeAlias.IsNullOrWhiteSpace() ? - Constants.Security.DefaultMemberTypeAlias : user.MemberTypeAlias); + user.MemberTypeAlias.IsNullOrWhiteSpace() ? Constants.Security.DefaultMemberTypeAlias : user.MemberTypeAlias); UpdateMemberProperties(member, user); - //TODO: do we want to accept empty passwords here - if third-party for example? In other method if so? + // TODO: do we want to accept empty passwords here - if third-party for example? + // In other method if so? _memberService.Save(member); - //re-assign id + // re-assign id user.Id = member.Id; - // TODO: do we need this? // TODO: [from backofficeuser] we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. //bool isLoginsPropertyDirty = member.IsPropertyDirty(nameof(UmbracoMembersIdentityUser.Logins)); @@ -72,18 +92,21 @@ namespace Umbraco.Infrastructure.Members // x.UserData))); //} - if (!member.HasIdentity) throw new DataException("Could not create the user, check logs for details"); + if (!member.HasIdentity) + { + throw new DataException("Could not create the member, check logs for details"); + } return Task.FromResult(IdentityResult.Success); - //TODO: confirm + // TODO: confirm and implement //if (memberUser.LoginsChanged) //{ // var logins = await GetLoginsAsync(memberUser); // _externalLoginStore.SaveUserLogins(member.Id, logins); //} - //TODO: confirm + // TODO: confirm and implement //if (memberUser.RolesChanged) //{ //IMembershipRoleService memberRoleService = _memberService; @@ -102,24 +125,23 @@ namespace Umbraco.Infrastructure.Members private bool UpdateMemberProperties(IMember member, UmbracoMembersIdentityUser memberIdentityUser) { - //[Comments as per BackOfficeUserStore & identity package] var anythingChanged = false; - //don't assign anything if nothing has changed as this will trigger the track changes of the model - if ( - //memberIdentityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Name)) && + + // don't assign anything if nothing has changed as this will trigger the track changes of the model + if (memberIdentityUser.IsPropertyDirty(nameof(UmbracoMembersIdentityUser.Name)) && member.Name != memberIdentityUser.Name && memberIdentityUser.Name.IsNullOrWhiteSpace() == false) { anythingChanged = true; member.Name = memberIdentityUser.Name; } - if ( - //memberIdentityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Email)) && + + if (memberIdentityUser.IsPropertyDirty(nameof(UmbracoMembersIdentityUser.Email)) && member.Email != memberIdentityUser.Email && memberIdentityUser.Email.IsNullOrWhiteSpace() == false) { anythingChanged = true; member.Email = memberIdentityUser.Email; } - + if (member.IsLockedOut != memberIdentityUser.IsLockedOut) { anythingChanged = true; @@ -127,22 +149,20 @@ namespace Umbraco.Infrastructure.Members if (member.IsLockedOut) { - //need to set the last lockout date + // need to set the last lockout date member.LastLockoutDate = DateTime.Now; } } - if ( - //memberIdentityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.UserName)) && + + if (memberIdentityUser.IsPropertyDirty(nameof(UmbracoMembersIdentityUser.UserName)) && member.Username != memberIdentityUser.UserName && memberIdentityUser.UserName.IsNullOrWhiteSpace() == false) { anythingChanged = true; member.Username = memberIdentityUser.UserName; } - - if ( - //member.IsPropertyDirty(nameof(BackOfficeIdentityUser.PasswordHash))&& - member.RawPasswordValue != memberIdentityUser.PasswordHash - && memberIdentityUser.PasswordHash.IsNullOrWhiteSpace() == false) + + if (memberIdentityUser.IsPropertyDirty(nameof(UmbracoMembersIdentityUser.PasswordHash)) + && member.RawPasswordValue != memberIdentityUser.PasswordHash && memberIdentityUser.PasswordHash.IsNullOrWhiteSpace() == false) { anythingChanged = true; member.RawPasswordValue = memberIdentityUser.PasswordHash; @@ -151,7 +171,7 @@ namespace Umbraco.Infrastructure.Members // TODO: Roles // [Comment] Same comment as per BackOfficeUserStore: Fix this for Groups too - //if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Roles)) || identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Groups))) + //if (identityUser.IsPropertyDirty(nameof(UmbracoMembersIdentityUser.Roles)) || identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Groups))) //{ // var userGroupAliases = member.Groups.Select(x => x.Alias).ToArray(); @@ -182,9 +202,7 @@ namespace Umbraco.Infrastructure.Members // } //} - //TODO: reset all changes - //memberIdentityUser.ResetDirtyProperties(false); - + memberIdentityUser.ResetDirtyProperties(false); return anythingChanged; } @@ -200,16 +218,17 @@ namespace Umbraco.Infrastructure.Members public async Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) { - //TODO: confirm logic + // TODO: confirm logic cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - var member = _memberService.GetByUsername(normalizedUserName); + + IMember member = _memberService.GetByUsername(normalizedUserName); if (member == null) { return null; } - var result = _mapper.Map(member); + UmbracoMembersIdentityUser result = _mapper.Map(member); return await Task.FromResult(result); } @@ -223,64 +242,147 @@ namespace Umbraco.Infrastructure.Members { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } return Task.FromResult(user.Id.ToString()); } public Task GetUserNameAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) { - //TODO: unit tests for and implement all bodies + // TODO: unit tests for and implement all bodies cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } return Task.FromResult(user.UserName); } - public Task SetNormalizedUserNameAsync(UmbracoMembersIdentityUser user, string normalizedName, CancellationToken cancellationToken) - { - return SetUserNameAsync(user, normalizedName, cancellationToken); throw new NotImplementedException(); - } + /// + /// Sets the normalized user name + /// + /// The member identity user + /// The normalized member name + /// The cancellation token + /// A task once complete + public Task SetNormalizedUserNameAsync(UmbracoMembersIdentityUser user, string normalizedName, CancellationToken cancellationToken) => SetUserNameAsync(user, normalizedName, cancellationToken); + /// + /// Sets the user name as an async operation + /// + /// The member identity user + /// The member user name + /// The cancellation token + /// A task once complete public Task SetUserNameAsync(UmbracoMembersIdentityUser user, string userName, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } user.UserName = userName; return Task.CompletedTask; } - public Task UpdateAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) + /// + /// Update the user asynchronously + /// + /// The member identity user + /// The cancellation token + /// An identity result task + public Task UpdateAsync(UmbracoMembersIdentityUser member, CancellationToken cancellationToken) { - throw new NotImplementedException(); + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (member == null) + { + throw new ArgumentNullException(nameof(member)); + } + + Attempt asInt = member.Id.TryConvertTo(); + if (asInt == false) + { + throw new InvalidOperationException("The member id must be an integer to work with the Umbraco"); + } + + using (IScope scope = _scopeProvider.CreateScope()) + { + IMember found = _memberService.GetById(asInt.Result); + if (found != null) + { + // we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. + var isLoginsPropertyDirty = member.IsPropertyDirty(nameof(BackOfficeIdentityUser.Logins)); + + if (UpdateMemberProperties(found, member)) + { + _memberService.Save(found); + } + + //if (isLoginsPropertyDirty) + //{ + // _externalLoginService.Save( + // found.Id, + // member.Logins.Select(x => new ExternalLogin( + // x.LoginProvider, + // x.ProviderKey, + // x.UserData))); + //} + } + + scope.Complete(); + } + + return Task.FromResult(IdentityResult.Success); } private void ThrowIfDisposed() { if (_disposed) + { throw new ObjectDisposedException(GetType().Name); + } } - ///TODO: All from BackOfficeUserStore - same. Can we share? + /// TODO: All from BackOfficeUserStore - same. Can we share? /// /// Set the user password hash /// - /// - /// - /// + /// The identity member user + /// The password hash + /// The cancellation token + /// Throws if the properties are null + /// Returns asynchronously public Task SetPasswordHashAsync(UmbracoMembersIdentityUser user, string passwordHash, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - if (passwordHash == null) throw new ArgumentNullException(nameof(passwordHash)); - if (string.IsNullOrEmpty(passwordHash)) throw new ArgumentException("Value can't be empty.", nameof(passwordHash)); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (passwordHash == null) + { + throw new ArgumentNullException(nameof(passwordHash)); + } + + if (string.IsNullOrEmpty(passwordHash)) + { + throw new ArgumentException("Value can't be empty.", nameof(passwordHash)); + } user.PasswordHash = passwordHash; - user.PasswordConfig = null; // Clear this so that it's reset at the repository level + + // Clear this so that it's reset at the repository level + user.PasswordConfig = null; return Task.CompletedTask; } @@ -290,12 +392,16 @@ namespace Umbraco.Infrastructure.Members /// /// /// + /// /// public Task GetPasswordHashAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } return Task.FromResult(user.PasswordHash); } @@ -303,17 +409,19 @@ namespace Umbraco.Infrastructure.Members /// /// Returns true if a user has a password set /// - /// - /// - /// + /// The identity user + /// The cancellation token + /// True if the user has a password public Task HasPasswordAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } return Task.FromResult(string.IsNullOrEmpty(user.PasswordHash) == false); } - } } diff --git a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs index 4ae0458910..24c15957e2 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; @@ -109,19 +109,20 @@ namespace Umbraco.Core.Services.Implement /// Email of the Member to create /// Name of the Member to create /// Alias of the MemberType the Member should be based on + /// Thrown when a member type for the given alias isn't found /// public IMember CreateMember(string username, string email, string name, string memberTypeAlias) { - var memberType = GetMemberType(memberTypeAlias); + IMemberType memberType = GetMemberType(memberTypeAlias); if (memberType == null) + { throw new ArgumentException("No member type with that alias.", nameof(memberTypeAlias)); + } var member = new Member(name, email.ToLower().Trim(), username, memberType); - using (var scope = ScopeProvider.CreateScope()) - { - CreateMember(scope, member, 0, false); - scope.Complete(); - } + using IScope scope = ScopeProvider.CreateScope(); + CreateMember(scope, member, 0, false); + scope.Complete(); return member; } @@ -312,7 +313,9 @@ namespace Umbraco.Core.Services.Implement // if saving is cancelled, media remains without an identity var saveEventArgs = new SaveEventArgs(member); if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs)) + { return; + } _memberRepository.Save(member); @@ -321,7 +324,9 @@ namespace Umbraco.Core.Services.Implement } if (withIdentity == false) + { return; + } Audit(AuditType.New, member.CreatorId, member.Id, $"Member '{member.Name}' was created with Id {member.Id}"); } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserStoreTests.cs index 59334d763c..5f8a143329 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserStoreTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserStoreTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -10,6 +10,7 @@ using NUnit.Framework; using Umbraco.Core.Mapping; using Umbraco.Core.Members; using Umbraco.Core.Models; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Infrastructure.Members; using Umbraco.Tests.UnitTests.Umbraco.Core.ShortStringHelper; @@ -26,7 +27,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Members _mockMemberService = new Mock(); return new UmbracoMembersUserStore( _mockMemberService.Object, - new UmbracoMapper(new MapDefinitionCollection(new List()))); + new UmbracoMapper(new MapDefinitionCollection(new List())), + new Mock().Object); } [Test] diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs index aef6abdd5e..ec8f09bc17 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs @@ -1,15 +1,10 @@ using System; using System.Linq; -using System.Net; -using System.Net.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; using Umbraco.Core.Dictionary; using Umbraco.Core.Events; -using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; @@ -54,14 +49,22 @@ namespace Umbraco.Web.BackOffice.Controllers _serializer = serializer; } + /// + /// Handles if the content for the specified ID isn't found + /// + /// The content ID to find + /// Whether to throw an exception + /// The error response protected NotFoundObjectResult HandleContentNotFound(object id, bool throwException = true) { ModelState.AddModelError("id", $"content with id: {id} was not found"); - var errorResponse = NotFound(ModelState); + NotFoundObjectResult errorResponse = NotFound(ModelState); + if (throwException) { throw new HttpResponseException(errorResponse); } + return errorResponse; } @@ -78,7 +81,7 @@ namespace Umbraco.Web.BackOffice.Controllers where TSaved : IContentSave { // map the property values - foreach (var propertyDto in dto.Properties) + foreach (ContentPropertyDto propertyDto in dto.Properties) { // get the property editor if (propertyDto.PropertyEditor == null) @@ -89,48 +92,63 @@ namespace Umbraco.Web.BackOffice.Controllers // get the value editor // nothing to save/map if it is readonly - var valueEditor = propertyDto.PropertyEditor.GetValueEditor(); - if (valueEditor.IsReadOnly) continue; + IDataValueEditor valueEditor = propertyDto.PropertyEditor.GetValueEditor(); + if (valueEditor.IsReadOnly) + { + continue; + } // get the property - var property = contentItem.PersistedContent.Properties[propertyDto.Alias]; + IProperty property = contentItem.PersistedContent.Properties[propertyDto.Alias]; // prepare files, if any matching property and culture - var files = contentItem.UploadedFiles + ContentPropertyFile[] files = contentItem.UploadedFiles .Where(x => x.PropertyAlias == propertyDto.Alias && x.Culture == propertyDto.Culture && x.Segment == propertyDto.Segment) .ToArray(); - foreach (var file in files) + foreach (ContentPropertyFile file in files) + { file.FileName = file.FileName.ToSafeFileName(ShortStringHelper); + } // create the property data for the property editor var data = new ContentPropertyData(propertyDto.Value, propertyDto.DataType.Configuration) { ContentKey = contentItem.PersistedContent.Key, PropertyTypeKey = property.PropertyType.Key, - Files = files + Files = files }; // let the editor convert the value that was received, deal with files, etc - var value = valueEditor.FromEditor(data, getPropertyValue(contentItem, property)); + object value = valueEditor.FromEditor(data, getPropertyValue(contentItem, property)); // set the value - tags are special - var tagAttribute = propertyDto.PropertyEditor.GetTagAttribute(); + TagsPropertyEditorAttribute tagAttribute = propertyDto.PropertyEditor.GetTagAttribute(); if (tagAttribute != null) { - var tagConfiguration = ConfigurationEditor.ConfigurationAs(propertyDto.DataType.Configuration); - if (tagConfiguration.Delimiter == default) tagConfiguration.Delimiter = tagAttribute.Delimiter; + TagConfiguration tagConfiguration = ConfigurationEditor.ConfigurationAs(propertyDto.DataType.Configuration); + if (tagConfiguration.Delimiter == default) + { + tagConfiguration.Delimiter = tagAttribute.Delimiter; + } + var tagCulture = property.PropertyType.VariesByCulture() ? culture : null; property.SetTagsValue(_serializer, value, tagConfiguration, tagCulture); } else + { savePropertyValue(contentItem, property, value); + } } } + /// + /// Handles if the state is invalid + /// + /// The model state to display protected virtual void HandleInvalidModelState(IErrorModel display) { - //lastly, if it is not valid, add the model state to the outgoing object and throw a 403 + // lastly, if it is not valid, add the model state to the outgoing object and throw a 403 if (!ModelState.IsValid) { display.Errors = ModelState.ToErrorDictionary(); @@ -151,38 +169,45 @@ namespace Umbraco.Web.BackOffice.Controllers /// protected TPersisted GetObjectFromRequest(Func getFromService) { - //checks if the request contains the key and the item is not null, if that is the case, return it from the request, otherwise return + // checks if the request contains the key and the item is not null, if that is the case, return it from the request, otherwise return // it from the callback return HttpContext.Items.ContainsKey(typeof(TPersisted).ToString()) && HttpContext.Items[typeof(TPersisted).ToString()] != null - ? (TPersisted) HttpContext.Items[typeof (TPersisted).ToString()] + ? (TPersisted)HttpContext.Items[typeof(TPersisted).ToString()] : getFromService(); } /// /// Returns true if the action passed in means we need to create something new /// - /// - /// - internal static bool IsCreatingAction(ContentSaveAction action) - { - return (action.ToString().EndsWith("New")); - } + /// The content action + /// Returns true if this is a creating action + internal static bool IsCreatingAction(ContentSaveAction action) => action.ToString().EndsWith("New"); - protected void AddCancelMessage(INotificationModel display, - string header = "speechBubbles/operationCancelledHeader", - string message = "speechBubbles/operationCancelledText", - bool localizeHeader = true, + /// + /// Adds a cancelled message to the display + /// + /// + /// + /// + /// + /// + /// + /// + protected void AddCancelMessage(INotificationModel display, string header = "speechBubbles/operationCancelledHeader", string message = "speechBubbles/operationCancelledText", bool localizeHeader = true, bool localizeMessage = true, string[] headerParams = null, string[] messageParams = null) { - //if there's already a default event message, don't add our default one - var msgs = EventMessages; - if (msgs != null && msgs.GetOrDefault().GetAll().Any(x => x.IsDefaultEventMessage)) return; + // if there's already a default event message, don't add our default one + IEventMessagesFactory messages = EventMessages; + if (messages != null && messages.GetOrDefault().GetAll().Any(x => x.IsDefaultEventMessage)) + { + return; + } display.AddWarningNotification( localizeHeader ? LocalizedTextService.Localize(header, headerParams) : header, - localizeMessage ? LocalizedTextService.Localize(message, messageParams): message); + localizeMessage ? LocalizedTextService.Localize(message, messageParams) : message); } } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index 629e2282ff..efaed903f2 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -7,8 +7,8 @@ using System.Net.Http; using System.Net.Mime; using System.Text; using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Umbraco.Core; @@ -17,6 +17,7 @@ using Umbraco.Core.Events; using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Models.ContentEditing; +using Umbraco.Core.Models.Membership; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Security; using Umbraco.Core.Serialization; @@ -54,9 +55,25 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly IUmbracoMembersUserManager _memberManager; private readonly IDataTypeService _dataTypeService; private readonly ILocalizedTextService _localizedTextService; - private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; private readonly IJsonSerializer _jsonSerializer; + /// + /// Initializes a new instance of the class. + /// + /// The culture dictionary + /// The logger factory + /// The string helper + /// The event messages factory + /// The entry point for localizing key services + /// The property editors + /// The mapper + /// The member service + /// The member type service + /// The member manager + /// The data-type service + /// The back office security accessor + /// The JSON serializer public MemberController( ICultureDictionary cultureDictionary, ILoggerFactory loggerFactory, @@ -69,7 +86,7 @@ namespace Umbraco.Web.BackOffice.Controllers IMemberTypeService memberTypeService, IUmbracoMembersUserManager memberManager, IDataTypeService dataTypeService, - IBackOfficeSecurityAccessor backofficeSecurityAccessor, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IJsonSerializer jsonSerializer) : base(cultureDictionary, loggerFactory, shortStringHelper, eventMessages, localizedTextService, jsonSerializer) { @@ -80,10 +97,21 @@ namespace Umbraco.Web.BackOffice.Controllers _memberManager = memberManager; _dataTypeService = dataTypeService; _localizedTextService = localizedTextService; - _backofficeSecurityAccessor = backofficeSecurityAccessor; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; _jsonSerializer = jsonSerializer; } + /// + /// The paginated list of members + /// + /// The page number to display + /// The size of the page + /// The ordering of the member list + /// The direction of the member list + /// The system field to order by + /// The current filter for the list + /// The member type + /// The paged result of members public PagedResult GetPagedResults( int pageNumber = 1, int pageSize = 100, @@ -123,11 +151,11 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// Returns a display node with a list view to render members /// - /// - /// + /// The member type to list + /// The member list for display public MemberListDisplay GetListNodeDisplay(string listName) { - var foundType = _memberTypeService.Get(listName); + IMemberType foundType = _memberTypeService.Get(listName); var name = foundType != null ? foundType.Name : listName; var apps = new List @@ -159,25 +187,26 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// Gets the content json for the member /// - /// - /// + /// The Guid key of the member + /// The member for display [OutgoingEditorModelEvent] public MemberDisplay GetByKey(Guid key) { - //TODO: this is not finding the key currently + // TODO: this is not finding the key currently IMember foundMember = _memberService.GetByKey(key); if (foundMember == null) { HandleContentNotFound(key); } + return _umbracoMapper.Map(foundMember); } /// /// Gets an empty content item for the /// - /// - /// + /// The content type + /// The empty member for display [OutgoingEditorModelEvent] public MemberDisplay GetEmpty(string contentTypeAlias = null) { @@ -202,91 +231,109 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// Saves member /// - /// + /// The content item to save as a member + /// The resulting member display object [FileUploadCleanupFilter] [OutgoingEditorModelEvent] [MemberSaveValidation] - public async Task> PostSave( - [ModelBinder(typeof(MemberBinder))] - MemberSave contentItem) + public async Task> PostSave([ModelBinder(typeof(MemberBinder))] MemberSave contentItem) { - if (contentItem == null) throw new ArgumentNullException(nameof(contentItem)); + if (contentItem == null) + { + throw new ArgumentNullException(nameof(contentItem)); + } if (ModelState.IsValid == false) { throw new HttpResponseException(HttpStatusCode.BadRequest, ModelState); } - //If we've reached here it means: + // If we've reached here it means: // * Our model has been bound // * and validated // * any file attachments have been saved to their temporary location for us to use // * we have a reference to the DTO object and the persisted object // * Permissions are valid - //map the properties to the persisted entity + // map the properties to the persisted entity MapPropertyValues(contentItem); - UmbracoMembersIdentityUser identityMember = ValidateMemberData(contentItem); + ValidateMemberData(contentItem); - //Unlike content/media - if there are errors for a member, we do NOT proceed to save them, we cannot so return the errors + // Unlike content/media - if there are errors for a member, we do NOT proceed to save them, we cannot so return the errors if (ModelState.IsValid == false) { - var forDisplay = _umbracoMapper.Map(contentItem.PersistedContent); + MemberDisplay forDisplay = _umbracoMapper.Map(contentItem.PersistedContent); forDisplay.Errors = ModelState.ToErrorDictionary(); throw HttpResponseException.CreateValidationErrorResponse(forDisplay); } - //We're gonna look up the current roles now because the below code can cause + IMemberType memberType = _memberTypeService.Get(contentItem.ContentTypeAlias); + if (memberType == null) + { + throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}"); + } + + // Create the member with the MemberManager + var identityMember = UmbracoMembersIdentityUser.CreateNew( + contentItem.Username, + contentItem.Email, + memberType.Alias, + contentItem.Name); + + // We're gonna look up the current roles now because the below code can cause // events to be raised and developers could be manually adding roles to members in // their handlers. If we don't look this up now there's a chance we'll just end up // removing the roles they've assigned. - var currRoles = _memberService.GetAllRoles(contentItem.PersistedContent.Username); + IEnumerable currentRoles = _memberService.GetAllRoles(contentItem.PersistedContent.Username); - //find the ones to remove and remove them - IEnumerable roles = currRoles.ToList(); - var rolesToRemove = roles.Except(contentItem.Groups).ToArray(); + // find the ones to remove and remove them + IEnumerable roles = currentRoles.ToList(); + string[] rolesToRemove = roles.Except(contentItem.Groups).ToArray(); - //Depending on the action we need to first do a create or update using the membership manager - //this ensures that passwords are formatted correctly and also performs the validation on the provider itself. + // Depending on the action we need to first do a create or update using the membership manager + // this ensures that passwords are formatted correctly and also performs the validation on the provider itself. switch (contentItem.Action) { case ContentSaveAction.Save: UpdateMemberData(contentItem); break; case ContentSaveAction.SaveNew: - await CreateMemberAsync(contentItem, identityMember); + IdentityResult identityResult = await CreateMemberAsync(contentItem, identityMember); break; default: - //we don't support anything else for members + // we don't support anything else for members throw new HttpResponseException(HttpStatusCode.NotFound); } - //TODO: There's 3 things saved here and we should do this all in one transaction, which we can do here by wrapping in a scope + // TODO: There's 3 things saved here and we should do this all in one transaction, + // which we can do here by wrapping in a scope // but it would be nicer to have this taken care of within the Save method itself - //Now let's do the role provider stuff - now that we've saved the content item (that is important since + // Now let's do the role provider stuff - now that we've saved the content item (that is important since // if we are changing the username, it must be persisted before looking up the member roles). if (rolesToRemove.Any()) { _memberService.DissociateRoles(new[] { contentItem.PersistedContent.Username }, rolesToRemove); } - //find the ones to add and add them - var toAdd = contentItem.Groups.Except(roles).ToArray(); + + // find the ones to add and add them + string[] toAdd = contentItem.Groups.Except(roles).ToArray(); if (toAdd.Any()) { - //add the ones submitted + // add the ones submitted _memberService.AssignRoles(new[] { contentItem.PersistedContent.Username }, toAdd); } - //return the updated model - var display = _umbracoMapper.Map(contentItem.PersistedContent); + // return the updated model + MemberDisplay display = _umbracoMapper.Map(contentItem.PersistedContent); - //lastly, if it is not valid, add the model state to the outgoing object and throw a 403 + // lastly, if it is not valid, add the model state to the outgoing object and throw a 403 HandleInvalidModelState(display); - var localizedTextService = _localizedTextService; - //put the correct messages in + ILocalizedTextService localizedTextService = _localizedTextService; + + // put the correct messages in switch (contentItem.Action) { case ContentSaveAction.Save: @@ -303,77 +350,64 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// Maps the property values to the persisted entity /// - /// + /// The member content item to map properties from private void MapPropertyValues(MemberSave contentItem) { - //Don't update the name if it is empty + // Don't update the name if it is empty if (contentItem.Name.IsNullOrWhiteSpace() == false) { contentItem.PersistedContent.Name = contentItem.Name; } - //map the custom properties - this will already be set for new entities in our member binder + // map the custom properties - this will already be set for new entities in our member binder contentItem.PersistedContent.Email = contentItem.Email; contentItem.PersistedContent.Username = contentItem.Username; - //use the base method to map the rest of the properties - base.MapPropertyValuesForPersistence( + // use the base method to map the rest of the properties + MapPropertyValuesForPersistence( contentItem, contentItem.PropertyCollectionDto, - (save, property) => property.GetValue(), //get prop val - (save, property, v) => property.SetValue(v), //set prop val + (save, property) => property.GetValue(), // get prop val + (save, property, v) => property.SetValue(v), // set prop val null); // member are all invariant } /// /// Create a member from the supplied member content data - /// All member password processing and creation is done via the aspnet identity MemberUserManager + /// + /// All member password processing and creation is done via the identity manager /// - /// - /// - /// - private async Task CreateMemberAsync(MemberSave contentItem, UmbracoMembersIdentityUser identityMember) + /// Member content data + /// The identity member to update + /// The identity result of the created member + private async Task CreateMemberAsync(MemberSave contentItem, UmbracoMembersIdentityUser identityMember) { - //var memberType = _memberTypeService.Get(contentItem.ContentTypeAlias); - //if (memberType == null) - // throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}"); - //var member = new Member(contentItem.Name, contentItem.Email, contentItem.Username, memberType, true) - //{ - // CreatorId = _backofficeSecurityAccessor.BackofficeSecurity.CurrentUser.Id, - // RawPasswordValue = _passwordSecurity.HashPasswordForStorage(contentItem.Password.NewPassword), - // Comments = contentItem.Comments, - // IsApproved = contentItem.IsApproved - //}; - - //return member; - IdentityResult created = await _memberManager.CreateAsync(identityMember, contentItem.Password.NewPassword); if (created.Succeeded == false) { throw HttpResponseException.CreateNotificationValidationErrorResponse(created.Errors.ToErrorMessage()); } - //now re-look the member back up which will now exist + // now re-look the member back up which will now exist IMember member = _memberService.GetByEmail(contentItem.Email); - member.CreatorId = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id; + member.CreatorId = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id; + // should this be removed since we've moved passwords out? - //since the back office user is creating this member, they will be set to approved + member.RawPasswordValue = identityMember.PasswordHash; + member.Comments = contentItem.Comments; + + // since the back office user is creating this member, they will be set to approved member.IsApproved = true; - //map the save info over onto the user + // map the save info over onto the user member = _umbracoMapper.Map(contentItem, member); contentItem.PersistedContent = member; + return created; } - private UmbracoMembersIdentityUser ValidateMemberData(MemberSave contentItem) + private void ValidateMemberData(MemberSave contentItem) { - var memberType = _memberTypeService.Get(contentItem.ContentTypeAlias); - if (memberType == null) - { - throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}"); - } - if (contentItem.Name.IsNullOrWhiteSpace()) { ModelState.AddPropertyError( @@ -399,14 +433,13 @@ namespace Umbraco.Web.BackOffice.Controllers if (contentItem.Password != null && !contentItem.Password.NewPassword.IsNullOrWhiteSpace()) { - //TODO: check password - //var validPassword = await _memberManager.CheckPasswordAsync(null, contentItem.Password.NewPassword); - //if (!validPassword) - //{ - // ModelState.AddPropertyError( - // new ValidationResult("Invalid password", new[] { "value" }), - // $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password"); - //} + Task> result = _memberManager.ValidatePassword(contentItem.Password.NewPassword); + if (result.Result.Exists(x => x.Succeeded == false)) + { + ModelState.AddPropertyError( + new ValidationResult($"Invalid password: {MapErrors(result.Result)}", new[] { "value" }), + $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password"); + } } else { @@ -414,48 +447,48 @@ namespace Umbraco.Web.BackOffice.Controllers new ValidationResult("Password cannot be empty", new[] { "value" }), $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password"); } + } - // Create the member with the MemberManager - var identityMember = UmbracoMembersIdentityUser.CreateNew( - contentItem.Username, - contentItem.Email, - memberType.Alias, - contentItem.Name); - //TODO: confirm where to do this - identityMember.RawPasswordValue = contentItem.Password.NewPassword; - return identityMember; + private string MapErrors(List result) + { + var sb = new StringBuilder(); + IEnumerable errors = result.Where(x => x.Succeeded == false); + + foreach (IdentityResult error in errors) + { + sb.AppendLine(error.Errors.ToErrorMessage()); + } + + return sb.ToString(); } /// /// Update the member security data - /// - /// - /// /// If the password has been reset then this method will return the reset/generated password, otherwise will return null. - /// - private async void UpdateMemberData(MemberSave memberSave) + /// + /// The member to save + private void UpdateMemberData(MemberSave memberSave) { - //TODO: optimise based on new member manager - memberSave.PersistedContent.WriterId = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id; + memberSave.PersistedContent.WriterId = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id; // If the user doesn't have access to sensitive values, then we need to check if any of the built in member property types // have been marked as sensitive. If that is the case we cannot change these persisted values no matter what value has been posted. // There's only 3 special ones we need to deal with that are part of the MemberSave instance: Comments, IsApproved, IsLockedOut // but we will take care of this in a generic way below so that it works for all props. - if (!_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.HasAccessToSensitiveData()) + if (!_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.HasAccessToSensitiveData()) { - var memberType = _memberTypeService.Get(memberSave.PersistedContent.ContentTypeId); + IMemberType memberType = _memberTypeService.Get(memberSave.PersistedContent.ContentTypeId); var sensitiveProperties = memberType .PropertyTypes.Where(x => memberType.IsSensitiveProperty(x.Alias)) .ToList(); - foreach (var sensitiveProperty in sensitiveProperties) + foreach (IPropertyType sensitiveProperty in sensitiveProperties) { - var destProp = memberSave.Properties.FirstOrDefault(x => x.Alias == sensitiveProperty.Alias); + ContentPropertyBasic destProp = memberSave.Properties.FirstOrDefault(x => x.Alias == sensitiveProperty.Alias); if (destProp != null) { - //if found, change the value of the contentItem model to the persisted value so it remains unchanged - var origValue = memberSave.PersistedContent.GetValue(sensitiveProperty.Alias); + // if found, change the value of the contentItem model to the persisted value so it remains unchanged + object origValue = memberSave.PersistedContent.GetValue(sensitiveProperty.Alias); destProp.Value = origValue; } } @@ -463,7 +496,7 @@ namespace Umbraco.Web.BackOffice.Controllers var isLockedOut = memberSave.IsLockedOut; - //if they were locked but now they are trying to be unlocked + // if they were locked but now they are trying to be unlocked if (memberSave.PersistedContent.IsLockedOut && isLockedOut == false) { memberSave.PersistedContent.IsLockedOut = false; @@ -471,34 +504,34 @@ namespace Umbraco.Web.BackOffice.Controllers } else if (!memberSave.PersistedContent.IsLockedOut && isLockedOut) { - //NOTE: This should not ever happen unless someone is mucking around with the request data. - //An admin cannot simply lock a user, they get locked out by password attempts, but an admin can un-approve them + // NOTE: This should not ever happen unless someone is mucking around with the request data. + // An admin cannot simply lock a user, they get locked out by password attempts, but an admin can un-approve them ModelState.AddModelError("custom", "An admin cannot lock a user"); } - //no password changes then exit ? - if (memberSave.Password == null) - return; - //TODO: update member password functionality in manager// set the password - - // set the password - memberSave.PersistedContent.RawPasswordValue = _memberManager.GeneratePassword(); + // no password changes then exit ? + if (memberSave.Password != null) + { + // set the password + memberSave.PersistedContent.RawPasswordValue = _memberManager.GeneratePassword(); + } } /// /// Permanently deletes a member /// - /// - /// + /// Guid of the member to delete + /// The result of the deletion /// [HttpPost] public IActionResult DeleteByKey(Guid key) { - var foundMember = _memberService.GetByKey(key); + IMember foundMember = _memberService.GetByKey(key); if (foundMember == null) { return HandleContentNotFound(key, false); } + _memberService.Delete(foundMember); return Ok(); @@ -512,19 +545,23 @@ namespace Umbraco.Web.BackOffice.Controllers [HttpGet] public IActionResult ExportMemberData(Guid key) { - var currentUser = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser; + IUser currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser; if (currentUser.HasAccessToSensitiveData() == false) { return Forbid(); } - var member = ((MemberService)_memberService).ExportMember(key); - if (member is null) throw new NullReferenceException("No member found with key " + key); + MemberExportModel member = ((MemberService)_memberService).ExportMember(key); + if (member is null) + { + throw new NullReferenceException("No member found with key " + key); + } var json = _jsonSerializer.Serialize(member); var fileName = $"{member.Name}_{member.Email}.txt"; + // Set custom header so umbRequestHelper.downloadFile can save the correct filename HttpContext.Response.Headers.Add("x-filename", fileName); diff --git a/src/umbraco.sln.DotSettings b/src/umbraco.sln.DotSettings index 2f99fe6350..6fb927035e 100644 --- a/src/umbraco.sln.DotSettings +++ b/src/umbraco.sln.DotSettings @@ -5,5 +5,6 @@ HINT False Default + True True True From 79dccc87b1c2a606c7985a9466834f6af2d8bae4 Mon Sep 17 00:00:00 2001 From: emmagarland Date: Sun, 6 Dec 2020 17:23:38 +0000 Subject: [PATCH 18/72] Enable change tracking via mapper. Added more unit tests for member controller. --- .../BackOffice/IdentityMapDefinition.cs | 8 +- .../Members/UmbracoMembersIdentityUser.cs | 3 +- .../AutoFixture/AutoMoqDataAttribute.cs | 3 +- .../Controllers/MemberControllerUnitTests.cs | 255 ++++++++++++++++++ .../Controllers/MemberController.cs | 10 +- .../Trees/MemberTreeController.cs | 3 +- 6 files changed, 266 insertions(+), 16 deletions(-) create mode 100644 src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs diff --git a/src/Umbraco.Core/BackOffice/IdentityMapDefinition.cs b/src/Umbraco.Core/BackOffice/IdentityMapDefinition.cs index b2702c1ddb..523876edc3 100644 --- a/src/Umbraco.Core/BackOffice/IdentityMapDefinition.cs +++ b/src/Umbraco.Core/BackOffice/IdentityMapDefinition.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.Extensions.Options; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; @@ -43,14 +43,14 @@ namespace Umbraco.Core.BackOffice (source, context) => { var target = new UmbracoMembersIdentityUser(); - //target.DisableChangeTracking(); + target.DisableChangeTracking(); return target; }, (source, target, context) => { Map(source, target); - //target.ResetDirtyProperties(true); - //target.EnableChangeTracking(); + target.ResetDirtyProperties(true); + target.EnableChangeTracking(); }); } diff --git a/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs b/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs index fb92edd47d..fd4687c664 100644 --- a/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs +++ b/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs @@ -107,8 +107,7 @@ namespace Umbraco.Core.Members string username, string email, string memberTypeAlias, - string name, - string password = null) + string name) { if (string.IsNullOrWhiteSpace(username)) { diff --git a/src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs b/src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs index 78d5d5554c..88605a7283 100644 --- a/src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs +++ b/src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Reflection; using AutoFixture; @@ -64,6 +64,7 @@ namespace Umbraco.Tests.UnitTests.AutoFixture .Customize(new ConstructorCustomization(typeof(UsersController), new GreedyConstructorQuery())) .Customize(new ConstructorCustomization(typeof(InstallController), new GreedyConstructorQuery())) .Customize(new ConstructorCustomization(typeof(PreviewController), new GreedyConstructorQuery())) + .Customize(new ConstructorCustomization(typeof(MemberController), new GreedyConstructorQuery())) .Customize(new ConstructorCustomization(typeof(BackOfficeController), new GreedyConstructorQuery())) .Customize(new ConstructorCustomization(typeof(BackOfficeUserManager), new GreedyConstructorQuery())); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs new file mode 100644 index 0000000000..9924084bca --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs @@ -0,0 +1,255 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using AutoFixture.NUnit3; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using Umbraco.Core.Cache; +using Umbraco.Core.Dictionary; +using Umbraco.Core.Events; +using Umbraco.Core.Mapping; +using Umbraco.Core.Members; +using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Security; +using Umbraco.Core.Serialization; +using Umbraco.Core.Services; +using Umbraco.Core.Strings; +using Umbraco.Infrastructure.Members; +using Umbraco.Tests.UnitTests.AutoFixture; +using Umbraco.Tests.UnitTests.Umbraco.Core.ShortStringHelper; +using Umbraco.Web; +using Umbraco.Web.BackOffice.Controllers; +using Umbraco.Web.Common.Exceptions; +using Umbraco.Web.Models; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers +{ + [TestFixture] + public class MemberControllerUnitTests + { + [Test] + [AutoMoqData] + public void PostSaveMember_WhenMemberIsNull_ExpectFailureResponse( + MemberController sut) + { + // arrange + // act + ArgumentNullException exception = Assert.ThrowsAsync(() => sut.PostSave(null)); + + // assert + //Assert.That(exception.Message, Is.EqualTo("Exception of type 'Umbraco.Web.Common.Exceptions.HttpResponse...")); + //Assert.That(exception.HResult, Is.EqualTo(42)); + } + + [Test] + [AutoMoqData] + public void PostSaveMember_WhenModelStateIsNotValid_ExpectFailureResponse( + MemberController sut) + { + // arrange + sut.ModelState.AddModelError("key", "Invalid model state"); + var fakeMemberData = new MemberSave() + { + Password = new ChangingPasswordModel() + { + Id = 123, + NewPassword = "i2ruf38vrba8^&T^", + OldPassword = null + } + }; + + // act + // assert + Assert.ThrowsAsync(() => sut.PostSave(fakeMemberData)); + } + + + [Test] + [AutoMoqData] + public async Task PostSaveMember_SaveNew_WhenAllIsSetupCorrectly_ExpectSuccessResponse( + [Frozen] IUmbracoMembersUserManager umbracoMembersUserManager, + IMemberTypeService memberTypeService, + IDataTypeService dataTypeService, + IMemberService memberService, + MapDefinitionCollection memberMapDefinition, + PropertyEditorCollection propertyEditorCollection, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IBackOfficeSecurity backOfficeSecurity) + { + // arrange + Member member = SetupMemberTestData(umbracoMembersUserManager, memberTypeService, memberMapDefinition, backOfficeSecurityAccessor, backOfficeSecurity, out UmbracoMapper mapper, out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew); + + Mock.Get(memberService).SetupSequence( + x => x.GetByEmail(It.IsAny())) + .Returns(() => null) + .Returns(() => member); + + MemberController sut = CreateSut(mapper, memberService, memberTypeService, umbracoMembersUserManager, dataTypeService, propertyEditorCollection, backOfficeSecurityAccessor); + + // act + ActionResult actualResult = await sut.PostSave(fakeMemberData); + + // assert + Assert.AreEqual(memberDisplay, actualResult.Value); + } + + [Test] + [AutoMoqData] + public async Task PostSaveMember_Save_WhenAllIsSetupCorrectly_ExpectSuccessResponse( + [Frozen] IUmbracoMembersUserManager umbracoMembersUserManager, + IMemberTypeService memberTypeService, + IDataTypeService dataTypeService, + IMemberService memberService, + MapDefinitionCollection memberMapDefinition, + PropertyEditorCollection propertyEditorCollection, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IBackOfficeSecurity backOfficeSecurity) + { + // arrange + Member member = SetupMemberTestData(umbracoMembersUserManager, memberTypeService, memberMapDefinition, backOfficeSecurityAccessor, backOfficeSecurity, out UmbracoMapper mapper, out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.Save); + + Mock.Get(memberService).SetupSequence( + x => x.GetByEmail(It.IsAny())) + .Returns(() => null) + .Returns(() => member); + + MemberController sut = CreateSut(mapper, memberService, memberTypeService, umbracoMembersUserManager, dataTypeService, propertyEditorCollection, backOfficeSecurityAccessor); + + // act + ActionResult actualResult = await sut.PostSave(fakeMemberData); + + // assert + Assert.AreEqual(memberDisplay, actualResult.Value); + } + + [Test] + [AutoMoqData] + public async Task PostSaveMember_SaveNew_WhenMemberEmailAlreadyExists_ExpectSuccessResponse( + [Frozen] IUmbracoMembersUserManager umbracoMembersUserManager, + IMemberTypeService memberTypeService, + IDataTypeService dataTypeService, + IMemberService memberService, + MapDefinitionCollection memberMapDefinition, + PropertyEditorCollection propertyEditorCollection, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IBackOfficeSecurity backOfficeSecurity) + { + // arrange + Member member = SetupMemberTestData(umbracoMembersUserManager, memberTypeService, memberMapDefinition, backOfficeSecurityAccessor, backOfficeSecurity, out UmbracoMapper mapper, out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew); + + Mock.Get(memberService).SetupSequence( + x => x.GetByEmail(It.IsAny())) + .Returns(() => member); + + MemberController sut = CreateSut(mapper, memberService, memberTypeService, umbracoMembersUserManager, dataTypeService, propertyEditorCollection, backOfficeSecurityAccessor); + + // act + HttpResponseException exception = Assert.ThrowsAsync(() => sut.PostSave(fakeMemberData)); + + // assert + //Assert.That(exception.Message, Is.EqualTo("Exception of type 'Umbraco.Web.Common.Exceptions.HttpResponse...")); + //Assert.That(exception.Value, Is.EqualTo(42)); + } + + /// + /// Setup all standard member data for test + /// + private Member SetupMemberTestData(IUmbracoMembersUserManager umbracoMembersUserManager, + IMemberTypeService memberTypeService, + MapDefinitionCollection memberMapDefinition, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IBackOfficeSecurity backOfficeSecurity, + out UmbracoMapper mapper, + out MemberSave fakeMemberData, + out MemberDisplay memberDisplay, + ContentSaveAction contentAction) + { + Mock.Get(umbracoMembersUserManager) + .Setup(x => x.CreateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(() => IdentityResult.Success); + Mock.Get(memberTypeService).Setup(x => x.GetDefault()).Returns("fakeAlias"); + Mock.Get(backOfficeSecurityAccessor).Setup(x => x.BackOfficeSecurity).Returns(backOfficeSecurity); + + var memberType = new MemberType(new DefaultShortStringHelper(new DefaultShortStringHelperConfig()), int.MinValue); + IMemberType testContentType = memberType; + + string fakePassword = "i2ruf38vrba8^&T^"; + var testName = "Test Name"; + var testEmail = "test@umbraco.com"; + var testUser = "TestUser"; + + var member = new Member(testName, testEmail, testUser, testContentType) {RawPasswordValue = fakePassword}; + mapper = new UmbracoMapper(memberMapDefinition); + + // TODO: reuse maps + mapper.Define((m, context) => new MemberDisplay() + { + Username = m.Username + }); + mapper.Define((m, context) => new Member(new Mock().Object)); + fakeMemberData = CreateFakeMemberData(member, contentAction); + + memberDisplay = new MemberDisplay() + { + }; + + return member; + } + + private MemberController CreateSut( + UmbracoMapper mapper, + IMemberService memberService, + IMemberTypeService memberTypeService, + IUmbracoMembersUserManager membersUserManager, + IDataTypeService dataTypeService, + PropertyEditorCollection propertyEditorCollection, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) => + new MemberController( + new DefaultCultureDictionary( + new Mock().Object, + new HttpRequestAppCache(() => null)), + new LoggerFactory(), + new MockShortStringHelper(), + new DefaultEventMessagesFactory( + new Mock().Object), + new Mock().Object, + propertyEditorCollection, + mapper, + memberService, + memberTypeService, + membersUserManager, + dataTypeService, + backOfficeSecurityAccessor, + new ConfigurationEditorJsonSerializer()); + + private static MemberSave CreateFakeMemberData(IMember member, ContentSaveAction action) + { + var fakeMemberData = new MemberSave() + { + Password = new ChangingPasswordModel() + { + Id = 123, + NewPassword = member.RawPasswordValue, + OldPassword = null + }, + Name = member.Name, + Email = member.Email, + Username = member.Username, + PersistedContent = member, + PropertyCollectionDto = new ContentPropertyCollectionDto() + { + }, + Groups = new List(), + Alias = "fakeAlias", + ContentTypeAlias = "fakeContentType", + Action = action + }; + return fakeMemberData; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index efaed903f2..ec0ea75986 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -192,7 +192,7 @@ namespace Umbraco.Web.BackOffice.Controllers [OutgoingEditorModelEvent] public MemberDisplay GetByKey(Guid key) { - // TODO: this is not finding the key currently + // TODO: this is not finding the key currently after member creation IMember foundMember = _memberService.GetByKey(key); if (foundMember == null) { @@ -441,12 +441,6 @@ namespace Umbraco.Web.BackOffice.Controllers $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password"); } } - else - { - ModelState.AddPropertyError( - new ValidationResult("Password cannot be empty", new[] { "value" }), - $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password"); - } } private string MapErrors(List result) @@ -512,7 +506,7 @@ namespace Umbraco.Web.BackOffice.Controllers // no password changes then exit ? if (memberSave.Password != null) { - // set the password + // TODO: set the password memberSave.PersistedContent.RawPasswordValue = _memberManager.GeneratePassword(); } } diff --git a/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs index 4ebd8f7cc5..dd82d73d62 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Http; @@ -63,6 +63,7 @@ namespace Umbraco.Web.BackOffice.Trees /// public ActionResult GetTreeNode(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings) { + //TODO: this is currently throwing an exception var node = GetSingleTreeNode(id, queryStrings); //add the tree alias to the node since it is standalone (has no root for which this normally belongs) From bcccbd3c73d7575925381eed14f182a7195f0cb3 Mon Sep 17 00:00:00 2001 From: emmagarland Date: Mon, 7 Dec 2020 17:40:21 +0000 Subject: [PATCH 19/72] Removed roles as not currently used for member --- src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs index d68b21dc1b..024511f8e4 100644 --- a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs +++ b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs @@ -4,11 +4,9 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Umbraco.Core; -using Umbraco.Core.BackOffice; using Umbraco.Core.Mapping; using Umbraco.Core.Members; using Umbraco.Core.Models; -using Umbraco.Core.Models.Identity; using Umbraco.Core.Scoping; using Umbraco.Core.Services; @@ -319,7 +317,7 @@ namespace Umbraco.Infrastructure.Members if (found != null) { // we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. - var isLoginsPropertyDirty = member.IsPropertyDirty(nameof(BackOfficeIdentityUser.Logins)); + // var isLoginsPropertyDirty = member.IsPropertyDirty(nameof(UmbracoMembersIdentityUser.Logins)); if (UpdateMemberProperties(found, member)) { From 3f0e7ab315789d5f92cd3740b6de8a9fbe9ced40 Mon Sep 17 00:00:00 2001 From: emmagarland Date: Tue, 8 Dec 2020 01:57:14 +0000 Subject: [PATCH 20/72] Merged and updated according to shared latest work, renamed to Members instead of UmbracoMembers. Tests currently red, fixing next. Empty appsettings again. --- .../IUmbracoMembersUserPasswordChecker.cs | 19 -- .../Members/UmbracoMembersIdentityUser.cs | 178 ----------- ...UmbracoMembersUserPasswordCheckerResult.cs | 12 - .../Models/Mapping/MemberMapDefinition.cs | 21 +- .../MembersUserPasswordCheckerResult.cs | 12 + src/Umbraco.Core/Umbraco.Core.csproj | 2 +- .../CoreMappingProfiles.cs | 1 + .../Members/IUmbracoMembersUserManager.cs | 62 ---- .../Members/UmbracoMembersIdentityOptions.cs | 11 - .../Members/UmbracoMembersUserManager.cs | 244 --------------- .../Security/IBackOfficeUserManager.cs | 1 + .../Security/IMembersUserManager.cs | 14 + .../Security/IMembersUserPasswordChecker.cs | 20 ++ .../Security/IUmbracoUserManager.cs | 18 +- .../Security/IdentityMapDefinition.cs | 16 +- .../MembersIdentityBuilder.cs} | 15 +- .../Security/MembersIdentityOptions.cs | 11 + .../Security/MembersIdentityUser.cs | 139 +++++++++ .../MembersUserStore.cs} | 82 ++--- .../Umbraco.Infrastructure.csproj | 4 + ...MembersServiceCollectionExtensionsTests.cs | 24 +- .../UmbracoMemberIdentityUserManagerTests.cs | 69 +++-- .../UmbracoMemberIdentityUserStoreTests.cs | 14 +- .../Controllers/MemberControllerUnitTests.cs | 15 +- .../Controllers/MemberController.cs | 187 ++++++------ .../UmbracoMemberIdentityBuilderExtensions.cs | 4 +- ...oMembersUserServiceCollectionExtensions.cs | 20 +- .../Security/MembersUserManager.cs | 286 ++++++++++++++++++ src/Umbraco.Web.UI.NetCore/appsettings.json | 4 +- 29 files changed, 738 insertions(+), 767 deletions(-) delete mode 100644 src/Umbraco.Core/Members/IUmbracoMembersUserPasswordChecker.cs delete mode 100644 src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs delete mode 100644 src/Umbraco.Core/Members/UmbracoMembersUserPasswordCheckerResult.cs create mode 100644 src/Umbraco.Core/Security/MembersUserPasswordCheckerResult.cs delete mode 100644 src/Umbraco.Infrastructure/Members/IUmbracoMembersUserManager.cs delete mode 100644 src/Umbraco.Infrastructure/Members/UmbracoMembersIdentityOptions.cs delete mode 100644 src/Umbraco.Infrastructure/Members/UmbracoMembersUserManager.cs create mode 100644 src/Umbraco.Infrastructure/Security/IMembersUserManager.cs create mode 100644 src/Umbraco.Infrastructure/Security/IMembersUserPasswordChecker.cs rename src/Umbraco.Infrastructure/{Members/UmbracoMembersIdentityBuilder.cs => Security/MembersIdentityBuilder.cs} (65%) create mode 100644 src/Umbraco.Infrastructure/Security/MembersIdentityOptions.cs create mode 100644 src/Umbraco.Infrastructure/Security/MembersIdentityUser.cs rename src/Umbraco.Infrastructure/{Members/UmbracoMembersUserStore.cs => Security/MembersUserStore.cs} (79%) rename src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/{Members => Security}/UmbracoMemberIdentityUserManagerTests.cs (65%) rename src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/{Members => Security}/UmbracoMemberIdentityUserStoreTests.cs (88%) create mode 100644 src/Umbraco.Web.Common/Security/MembersUserManager.cs diff --git a/src/Umbraco.Core/Members/IUmbracoMembersUserPasswordChecker.cs b/src/Umbraco.Core/Members/IUmbracoMembersUserPasswordChecker.cs deleted file mode 100644 index b361ca3121..0000000000 --- a/src/Umbraco.Core/Members/IUmbracoMembersUserPasswordChecker.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Threading.Tasks; - -namespace Umbraco.Core.Members -{ - /// - /// Used by the UmbracoMembersUserManager to check the username/password which allows for developers to more easily - /// set the logic for this procedure. - /// - public interface IUmbracoMembersUserPasswordChecker - { - /// - /// Checks a password for a member - /// - /// - /// - /// - Task CheckPasswordAsync(UmbracoMembersIdentityUser member, string password); - } -} diff --git a/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs b/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs deleted file mode 100644 index fd4687c664..0000000000 --- a/src/Umbraco.Core/Members/UmbracoMembersIdentityUser.cs +++ /dev/null @@ -1,178 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using Umbraco.Core.Models.Entities; - -namespace Umbraco.Core.Members -{ - /// - /// An Umbraco member user type - /// TODO: use of identity classes in future - /// - public class UmbracoMembersIdentityUser : IRememberBeingDirty - { - private int _id; - - private string _passwordHash; - - private string _passwordConfig; - - /// - /// Gets or sets the member name - /// - public string Name { get; set; } - - /// - /// Gets or sets the member email - /// - public string Email { get; set; } - - /// - /// Gets or sets the member username - /// - public string UserName { get; set; } - - /// - /// Gets or sets the alias of the member type - /// - public string MemberTypeAlias { get; set; } - - /// - /// Gets or sets a value indicating whether the member is locked out - /// - public bool IsLockedOut { get; set; } - - /// - /// Gets a value indicating whether an Id has been set on this object - /// This will be false if the object is new and not persisted to the database - /// - public bool HasIdentity { get; private set; } - - /// - /// Gets or sets the member Id - /// - public int Id - { - get => _id; - set - { - _id = value; - HasIdentity = true; - } - } - - /// - /// Gets or sets the salted/hashed form of the user password - /// - public string PasswordHash - { - get => _passwordHash; - set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordHash, nameof(PasswordHash)); - } - - /// - /// Gets or sets the password config - /// - public string PasswordConfig - { - get => _passwordConfig; - set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfig)); - } - - /// - /// Gets or sets a value indicating whether member Is Approved - /// - public bool IsApproved { get; set; } - - /// - /// Gets the for change tracking - /// - protected BeingDirty BeingDirty { get; } = new BeingDirty(); - - // TODO: implement as per base identity user - //public bool LoginsChanged; - //public bool RolesChanged; - - /// - /// Create a new identity member - /// - /// The member username - /// The member email - /// The member type alias - /// The member name - /// TODO: confirm The password (may be null in some instances) - /// Throws is username is null or whitespace - /// The identity member user - public static UmbracoMembersIdentityUser CreateNew( - string username, - string email, - string memberTypeAlias, - string name) - { - if (string.IsNullOrWhiteSpace(username)) - { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(username)); - } - - // no groups/roles yet - var member = new UmbracoMembersIdentityUser - { - UserName = username, - Email = email, - Name = name, - MemberTypeAlias = memberTypeAlias, - Id = 0, // TODO: is this meant to be 0 in this circumstance? - // false by default unless specifically set - HasIdentity = false - }; - - member.EnableChangeTracking(); - return member; - } - - /// - public event PropertyChangedEventHandler PropertyChanged - { - add => BeingDirty.PropertyChanged += value; - - remove => BeingDirty.PropertyChanged -= value; - } - - /// - - public bool IsDirty() => BeingDirty.IsDirty(); - - /// - public bool IsPropertyDirty(string propName) => BeingDirty.IsPropertyDirty(propName); - - /// - public IEnumerable GetDirtyProperties() => BeingDirty.GetDirtyProperties(); - - /// - - public void ResetDirtyProperties() => BeingDirty.ResetDirtyProperties(); - - /// - - public void DisableChangeTracking() => BeingDirty.DisableChangeTracking(); - - /// - - public void EnableChangeTracking() => BeingDirty.EnableChangeTracking(); - - /// - public bool WasDirty() => BeingDirty.WasDirty(); - - /// - public bool WasPropertyDirty(string propertyName) => BeingDirty.WasPropertyDirty(propertyName); - - /// - public void ResetWereDirtyProperties() => BeingDirty.ResetWereDirtyProperties(); - - /// - public void ResetDirtyProperties(bool rememberDirty) => BeingDirty.ResetDirtyProperties(rememberDirty); - - /// - public IEnumerable GetWereDirtyProperties() => BeingDirty.GetWereDirtyProperties(); - } -} diff --git a/src/Umbraco.Core/Members/UmbracoMembersUserPasswordCheckerResult.cs b/src/Umbraco.Core/Members/UmbracoMembersUserPasswordCheckerResult.cs deleted file mode 100644 index 8432b7b0bf..0000000000 --- a/src/Umbraco.Core/Members/UmbracoMembersUserPasswordCheckerResult.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Umbraco.Core.Members -{ - /// - /// The result returned from the IUmbracoMembersUserPasswordChecker - /// - public enum UmbracoMembersUserPasswordCheckerResult - { - ValidCredentials, - InvalidCredentials, - FallbackToDefaultChecker - } -} diff --git a/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs index bc8589e8ef..60fe4daace 100644 --- a/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs @@ -1,23 +1,18 @@ -using Umbraco.Core.Mapping; -using Umbraco.Core.Models; +using Umbraco.Core.Mapping; using Umbraco.Web.Models.ContentEditing; -namespace Umbraco.Web.Models.Mapping +namespace Umbraco.Core.Models.Mapping { + /// public class MemberMapDefinition : IMapDefinition { - public MemberMapDefinition() - { - } - - public void DefineMaps(UmbracoMapper mapper) - { - mapper.Define(Map); - } + /// + public void DefineMaps(UmbracoMapper mapper) => mapper.Define(Map); // mappers private static void Map(MemberSave source, IMember target, MapperContext context) { + // TODO: ensure all properties are mapped as required target.IsApproved = source.IsApproved; target.Name = source.Name; target.Email = source.Email; @@ -25,10 +20,6 @@ namespace Umbraco.Web.Models.Mapping target.Username = source.Username; target.Id = (int)(long)source.Id; target.Comments = source.Comments; - target.IsApproved = source.IsApproved; - - //TODO: ensure all properties are mapped as required - } } } diff --git a/src/Umbraco.Core/Security/MembersUserPasswordCheckerResult.cs b/src/Umbraco.Core/Security/MembersUserPasswordCheckerResult.cs new file mode 100644 index 0000000000..3212609ed9 --- /dev/null +++ b/src/Umbraco.Core/Security/MembersUserPasswordCheckerResult.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Core.Security +{ + /// + /// The result returned from the IMembersUserPasswordChecker + /// + public enum MembersUserPasswordCheckerResult + { + ValidCredentials, + InvalidCredentials, + FallbackToDefaultChecker + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 300dedc1c6..28695d1673 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 diff --git a/src/Umbraco.Infrastructure/Composing/CompositionExtensions/CoreMappingProfiles.cs b/src/Umbraco.Infrastructure/Composing/CompositionExtensions/CoreMappingProfiles.cs index 05cd6a74c8..72af10e31c 100644 --- a/src/Umbraco.Infrastructure/Composing/CompositionExtensions/CoreMappingProfiles.cs +++ b/src/Umbraco.Infrastructure/Composing/CompositionExtensions/CoreMappingProfiles.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Core.DependencyInjection; using Umbraco.Core.Mapping; +using Umbraco.Core.Models.Mapping; using Umbraco.Core.Security; using Umbraco.Web.Models.Mapping; diff --git a/src/Umbraco.Infrastructure/Members/IUmbracoMembersUserManager.cs b/src/Umbraco.Infrastructure/Members/IUmbracoMembersUserManager.cs deleted file mode 100644 index 8eedb67af6..0000000000 --- a/src/Umbraco.Infrastructure/Members/IUmbracoMembersUserManager.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -using Umbraco.Core.Members; - -namespace Umbraco.Infrastructure.Members -{ - public interface IUmbracoMembersUserManager : IUmbracoMembersUserManager - { - } - - public interface IUmbracoMembersUserManager : IDisposable where TUser : UmbracoMembersIdentityUser - { - /// - /// Creates the specified user in the backing store with given password, as an asynchronous operation. - /// - /// The member to create. - /// The new password - /// - /// The that represents the asynchronous operation, containing the - /// of the operation. - /// - Task CreateAsync(TUser memberUser, string password); - - /// - /// Helper method to generate a password for a user based on the current password validator - /// - /// Returns the generated password - string GeneratePassword(); - - /// - /// Adds the to the specified only if the user - /// does not already have a password. - /// - /// The member whose password should be set. - /// The password to set. - /// - /// The that represents the asynchronous operation, containing the - /// of the operation. - /// - Task AddPasswordAsync(TUser memberUser, string password); - - /// - /// Returns a flag indicating whether the given is valid for the - /// specified . - /// - /// The user whose password should be validated. - /// The password to validate - /// The that represents the asynchronous operation, containing true if - /// the specified matches the one store for the , - /// otherwise false. - Task CheckPasswordAsync(TUser memberUser, string password); - - /// - /// Method to validate the password without an identity user - /// - /// The password to validate - /// The result of the validation - Task> ValidatePassword(string password); - } -} diff --git a/src/Umbraco.Infrastructure/Members/UmbracoMembersIdentityOptions.cs b/src/Umbraco.Infrastructure/Members/UmbracoMembersIdentityOptions.cs deleted file mode 100644 index e72b2e3aba..0000000000 --- a/src/Umbraco.Infrastructure/Members/UmbracoMembersIdentityOptions.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.AspNetCore.Identity; - -namespace Umbraco.Infrastructure.Members -{ - /// - /// Identity options specifically for the Umbraco members identity implementation - /// - public class UmbracoMembersIdentityOptions : IdentityOptions - { - } -} diff --git a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserManager.cs b/src/Umbraco.Infrastructure/Members/UmbracoMembersUserManager.cs deleted file mode 100644 index 3ab230a76e..0000000000 --- a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserManager.cs +++ /dev/null @@ -1,244 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.Models; -using Umbraco.Core.Members; -using Umbraco.Core.Security; - - -namespace Umbraco.Infrastructure.Members -{ - /// - /// A manager for the Umbraco members identity implementation - /// - public class UmbracoMembersUserManager : UmbracoMembersUserManager, IUmbracoMembersUserManager - { - /// - public UmbracoMembersUserManager( - IUserStore store, - IOptions optionsAccessor, - IPasswordHasher passwordHasher, - IEnumerable> userValidators, - IEnumerable> passwordValidators, - ILookupNormalizer keyNormalizer, - IdentityErrorDescriber errors, - IServiceProvider services, - ILogger> logger, - IOptions passwordConfiguration) : - base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger, passwordConfiguration) - { - } - } - - /// - /// Manager for the member identity user - /// - /// The identity user - public class UmbracoMembersUserManager : UserManager - where T : UmbracoMembersIdentityUser - { - private PasswordGenerator _passwordGenerator; - - /// - /// Initializes a new instance of the class. - /// - /// The members store - /// The identity options accessor - /// The password hasher - /// The user validators - /// The password validators - /// The keep lookup normalizer - /// The error display messages - /// The service provider - /// The logger - /// The password configuration - public UmbracoMembersUserManager( - IUserStore store, - IOptions optionsAccessor, - IPasswordHasher passwordHasher, - IEnumerable> userValidators, - IEnumerable> passwordValidators, - ILookupNormalizer keyNormalizer, - IdentityErrorDescriber errors, - IServiceProvider services, - ILogger> logger, - IOptions passwordConfiguration) : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) => - PasswordConfiguration = passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration)); - - - /// - /// Gets or sets the password configuration - /// - public IPasswordConfiguration PasswordConfiguration { get; protected set; } - - /// - /// gets or sets the underlying options property with our own strongly typed version - /// - public new UmbracoMembersIdentityOptions Options - { - get => (UmbracoMembersIdentityOptions)base.Options; - set => base.Options = value; - } - - /// - /// Gets or sets the default Umbraco member user password checker - /// - public IUmbracoMembersUserPasswordChecker UmbracoMembersUserPasswordChecker { get; set; } - - /// - /// TODO: from BackOfficeUserManager duplicated, could be shared - /// Override to determine how to hash the password - /// - /// The member to validate - /// The new password - /// Whether to validate the password - /// The identity result of updating the password hash - /// - /// 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 UpdatePasswordHash(T memberUser, string newPassword, bool validatePassword) - { - // memberUser.LastPasswordChangeDateUtc = DateTime.UtcNow; - - if (validatePassword) - { - IdentityResult validate = await ValidatePasswordAsync(memberUser, newPassword); - if (!validate.Succeeded) - { - return validate; - } - } - - if (!(Store is IUserPasswordStore passwordStore)) - { - throw new NotSupportedException("The current user store does not implement " + typeof(IUserPasswordStore<>)); - } - - var hash = newPassword != null ? PasswordHasher.HashPassword(memberUser, newPassword) : null; - await passwordStore.SetPasswordHashAsync(memberUser, hash, CancellationToken); - await UpdateSecurityStampInternal(memberUser); - return IdentityResult.Success; - } - - /// TODO: duplicated code from backofficeusermanager, could be shared - /// - /// Logic used to validate a username and password - /// - /// The member to validate - /// The password to validate - /// Whether the password is the correct password for this member - /// - /// By default this uses the standard ASP.Net Identity approach which is: - /// * Get password store - /// * Call VerifyPasswordAsync with the password store + user + password - /// * Uses the PasswordHasher.VerifyHashedPassword to compare the stored password - /// - /// In some cases people want simple custom control over the username/password check, for simplicity - /// sake, developers would like the users to simply validate against an LDAP directory but the user - /// data remains stored inside of Umbraco. - /// See: http://issues.umbraco.org/issue/U4-7032 for the use cases. - /// - /// We've allowed this check to be overridden with a simple callback so that developers don't actually - /// have to implement/override this class. - /// - public override async Task CheckPasswordAsync(T member, string password) - { - if (UmbracoMembersUserPasswordChecker != null) - { - UmbracoMembersUserPasswordCheckerResult result = await UmbracoMembersUserPasswordChecker.CheckPasswordAsync(member, password); - - if (member.HasIdentity == false) - { - return false; - } - - // if the result indicates to not fallback to the default, then return true if the credentials are valid - if (result != UmbracoMembersUserPasswordCheckerResult.FallbackToDefaultChecker) - { - return result == UmbracoMembersUserPasswordCheckerResult.ValidCredentials; - } - } - - // we cannot proceed if the user passed in does not have an identity - if (member.HasIdentity == false) - { - return false; - } - - // use the default behavior - return await base.CheckPasswordAsync(member, password); - } - - /// TODO: from BackOfficeUserManager duplicated, could be shared - /// - /// This is copied from the underlying .NET base class since they decided to not expose it - /// - /// The user to update the security stamp for - /// Task returns - private async Task UpdateSecurityStampInternal(T user) - { - if (SupportsUserSecurityStamp == false) - { - return; - } - - await GetSecurityStore().SetSecurityStampAsync(user, NewSecurityStamp(), CancellationToken.None); - } - - /// TODO: from BackOfficeUserManager duplicated, could be shared - /// - /// This is copied from the underlying .NET base class since they decided to not expose it - /// - /// Return a user security stamp - private IUserSecurityStampStore GetSecurityStore() - { - if (!(Store is IUserSecurityStampStore store)) - { - throw new NotSupportedException("The current user store does not implement " + typeof(IUserSecurityStampStore<>)); - } - - return store; - } - - /// TODO: from BackOfficeUserManager duplicated, could be shared - /// - /// This is copied from the underlying .NET base class since they decided to not expose it - /// - /// Returns a new security stamp - private static string NewSecurityStamp() => Guid.NewGuid().ToString(); - - /// - /// TODO: from BackOfficeUserManager duplicated, could be shared - /// Helper method to generate a password for a member based on the current password validator - /// - /// The generated password - public string GeneratePassword() - { - _passwordGenerator ??= new PasswordGenerator(PasswordConfiguration); - string password = _passwordGenerator.GeneratePassword(); - return password; - } - - /// - /// Helper method to validate a password based on the current password validator - /// - /// The password to update - /// The validated password - public async Task> ValidatePassword(string password) - { - var passwordValidators = new List(); - foreach(IPasswordValidator validator in PasswordValidators) - { - IdentityResult result = await validator.ValidateAsync(this, null, password); - passwordValidators.Add(result); - } - - return passwordValidators; - } - } -} diff --git a/src/Umbraco.Infrastructure/Security/IBackOfficeUserManager.cs b/src/Umbraco.Infrastructure/Security/IBackOfficeUserManager.cs index 4235195bb1..cc0a63142b 100644 --- a/src/Umbraco.Infrastructure/Security/IBackOfficeUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/IBackOfficeUserManager.cs @@ -1,4 +1,5 @@ using Umbraco.Core.Security; +using Umbraco.Infrastructure.Security; namespace Umbraco.Core.Security { diff --git a/src/Umbraco.Infrastructure/Security/IMembersUserManager.cs b/src/Umbraco.Infrastructure/Security/IMembersUserManager.cs new file mode 100644 index 0000000000..a5b0579cb7 --- /dev/null +++ b/src/Umbraco.Infrastructure/Security/IMembersUserManager.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Umbraco.Core.Security; + +namespace Umbraco.Infrastructure.Security +{ + /// + /// The user manager for members + /// + public interface IMembersUserManager : IUmbracoUserManager + { + } +} diff --git a/src/Umbraco.Infrastructure/Security/IMembersUserPasswordChecker.cs b/src/Umbraco.Infrastructure/Security/IMembersUserPasswordChecker.cs new file mode 100644 index 0000000000..969b4feb79 --- /dev/null +++ b/src/Umbraco.Infrastructure/Security/IMembersUserPasswordChecker.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using Umbraco.Core.Security; + +namespace Umbraco.Infrastructure.Security +{ + /// + /// Used by the MembersUserManager to check the username/password which allows for developers to more easily + /// set the logic for this procedure. + /// + public interface IMembersUserPasswordChecker + { + /// + /// Checks a password for a member + /// + /// + /// TODO: what should our implementation be for members? + /// + Task CheckPasswordAsync(MembersIdentityUser user, string password); + } +} diff --git a/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs b/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs index 4bec4c9c7a..30b11cc8a8 100644 --- a/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs @@ -4,11 +4,12 @@ using System.Security.Claims; using System.Security.Principal; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; +using Umbraco.Core.Models.Identity; using Umbraco.Core.Models.Membership; using Umbraco.Core.Security; using Umbraco.Web.Models.ContentEditing; -namespace Umbraco.Core.Security +namespace Umbraco.Infrastructure.Security { /// @@ -16,7 +17,7 @@ namespace Umbraco.Core.Security /// /// The type of user public interface IUmbracoUserManager : IDisposable - where TUser : BackOfficeIdentityUser + where TUser : UmbracoIdentityUser { /// /// Gets the user id of a user @@ -223,6 +224,19 @@ namespace Umbraco.Core.Security /// Task CreateAsync(TUser user); + + /// + /// Creates the specified in the backing store with a password, + /// as an asynchronous operation. + /// + /// The user to create. + /// The password to add to the user. + /// + /// The that represents the asynchronous operation, containing the + /// of the operation. + /// + Task CreateAsync(TUser user, string password); + /// /// Generate a password for a user based on the current password validator /// diff --git a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs index 4f5a11e887..00239b21eb 100644 --- a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs +++ b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs @@ -3,10 +3,10 @@ using Microsoft.Extensions.Options; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Mapping; -using Umbraco.Core.Members; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; +using Umbraco.Infrastructure.Security; namespace Umbraco.Core.Security { @@ -39,10 +39,10 @@ namespace Umbraco.Core.Security target.EnableChangeTracking(); }); - mapper.Define( + mapper.Define( (source, context) => { - var target = new UmbracoMembersIdentityUser(); + var target = new MembersIdentityUser(source.Id); target.DisableChangeTracking(); return target; }, @@ -91,20 +91,20 @@ namespace Umbraco.Core.Security //target.Roles =; } - private void Map(IMember source, UmbracoMembersIdentityUser target) + private void Map(IMember source, MembersIdentityUser target) { target.Email = source.Email; target.UserName = source.Username; - //target.LastPasswordChangeDateUtc = source.LastPasswordChangeDate.ToUniversalTime(); - //target.LastLoginDateUtc = source.LastLoginDate.ToUniversalTime(); + target.LastPasswordChangeDateUtc = source.LastPasswordChangeDate.ToUniversalTime(); + target.LastLoginDateUtc = source.LastLoginDate.ToUniversalTime(); //target.EmailConfirmed = source.EmailConfirmedDate.HasValue; target.Name = source.Name; - //target.AccessFailedCount = source.FailedPasswordAttempts; + target.AccessFailedCount = source.FailedPasswordAttempts; target.PasswordHash = GetPasswordHash(source.RawPasswordValue); target.PasswordConfig = source.PasswordConfiguration; target.IsApproved = source.IsApproved; //target.SecurityStamp = source.SecurityStamp; - //target.LockoutEndDateUtc = source.IsLockedOut ? DateTime.MaxValue.ToUniversalTime() : (DateTime?)null; + target.LockoutEnd = source.IsLockedOut ? DateTime.MaxValue.ToUniversalTime() : (DateTime?)null; } private static string GetPasswordHash(string storedPass) diff --git a/src/Umbraco.Infrastructure/Members/UmbracoMembersIdentityBuilder.cs b/src/Umbraco.Infrastructure/Security/MembersIdentityBuilder.cs similarity index 65% rename from src/Umbraco.Infrastructure/Members/UmbracoMembersIdentityBuilder.cs rename to src/Umbraco.Infrastructure/Security/MembersIdentityBuilder.cs index e195dc925c..553cca6a17 100644 --- a/src/Umbraco.Infrastructure/Members/UmbracoMembersIdentityBuilder.cs +++ b/src/Umbraco.Infrastructure/Security/MembersIdentityBuilder.cs @@ -1,24 +1,23 @@ -using System; +using System; using System.Reflection; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; -using Umbraco.Core.Members; -namespace Umbraco.Infrastructure.Members +namespace Umbraco.Infrastructure.Security { - public class UmbracoMembersIdentityBuilder : IdentityBuilder + public class MembersIdentityBuilder : IdentityBuilder { - public UmbracoMembersIdentityBuilder(IServiceCollection services) : base(typeof(UmbracoMembersIdentityUser), services) + public MembersIdentityBuilder(IServiceCollection services) : base(typeof(MembersIdentityUser), services) { } - public UmbracoMembersIdentityBuilder(Type role, IServiceCollection services) : base(typeof(UmbracoMembersIdentityUser), role, services) + public MembersIdentityBuilder(Type role, IServiceCollection services) : base(typeof(MembersIdentityUser), role, services) { } /// - /// Adds a token provider for the . + /// Adds a token provider for the . /// /// The name of the provider to add. /// The type of the to add. @@ -29,7 +28,7 @@ namespace Umbraco.Infrastructure.Members { throw new InvalidOperationException($"Invalid Type for TokenProvider: {provider.FullName}"); } - Services.Configure(options => + Services.Configure(options => { options.Tokens.ProviderMap[providerName] = new TokenProviderDescriptor(provider); }); diff --git a/src/Umbraco.Infrastructure/Security/MembersIdentityOptions.cs b/src/Umbraco.Infrastructure/Security/MembersIdentityOptions.cs new file mode 100644 index 0000000000..0510096bb2 --- /dev/null +++ b/src/Umbraco.Infrastructure/Security/MembersIdentityOptions.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Identity; + +namespace Umbraco.Infrastructure.Security +{ + /// + /// Identity options specifically for the Umbraco members identity implementation + /// + public class MembersIdentityOptions : IdentityOptions + { + } +} diff --git a/src/Umbraco.Infrastructure/Security/MembersIdentityUser.cs b/src/Umbraco.Infrastructure/Security/MembersIdentityUser.cs new file mode 100644 index 0000000000..4e13e6839d --- /dev/null +++ b/src/Umbraco.Infrastructure/Security/MembersIdentityUser.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Identity; +using Umbraco.Core; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Models.Membership; + +namespace Umbraco.Infrastructure.Security +{ + /// + /// The identity user used for the member + /// + public class MembersIdentityUser : UmbracoIdentityUser + { + private string _name; + private string _passwordConfig; + private IReadOnlyCollection _groups; + + // TODO: reused from backoffice - share? + // Custom comparer for enumerables + private static readonly DelegateEqualityComparer> s_groupsComparer = new DelegateEqualityComparer>( + (groups, enumerable) => groups.Select(x => x.Alias).UnsortedSequenceEqual(enumerable.Select(x => x.Alias)), + groups => groups.GetHashCode()); + + + /// + /// Used to construct a new instance without an identity + /// + public static MembersIdentityUser CreateNew(string username, string email, string memberTypeAlias, + string name = null) + { + if (string.IsNullOrWhiteSpace(username)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(username)); + } + + var user = new MembersIdentityUser(); + user.DisableChangeTracking(); + user.UserName = username; + user.Email = email; + user.MemberTypeAlias = memberTypeAlias; + // TODO: confirm if should be approved + user.IsApproved = true; + user.Id = null; + user.HasIdentity = false; + user._name = name; + user.EnableChangeTracking(); + return user; + } + + /// + /// Initializes a new instance of the class. + /// + public MembersIdentityUser(int userId) + { + // use the property setters - they do more than just setting a field + Id = UserIdToString(userId); + } + + public MembersIdentityUser() + { + } + + /// + /// Gets or sets the member's real name + /// + public string Name + { + get => _name; + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); + } + + /// + /// Gets or sets the password config + /// + public string PasswordConfig + { + get => _passwordConfig; + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfig)); + } + + /// + /// Gets or sets the user groups + /// TBC: how to implement for members? + /// + public IReadOnlyCollection Groups + { + get => _groups; + set + { + _groups = value.Where(x => x.Alias != null).ToArray(); + + var roles = new List>(); + foreach (IdentityUserRole identityUserRole in _groups.Select(x => new IdentityUserRole + { + RoleId = x.Alias, + UserId = Id?.ToString() + })) + { + roles.Add(identityUserRole); + } + + // now reset the collection + Roles = roles; + + BeingDirty.SetPropertyValueAndDetectChanges(value, ref _groups, nameof(Groups), s_groupsComparer); + } + } + + /// + /// Gets a value indicating whether the member is locked out + /// + public bool IsLockedOut + { + get + { + var isLocked = LockoutEnd.HasValue && LockoutEnd.Value.ToLocalTime() >= DateTime.Now; + return isLocked; + } + } + + /// + /// Gets or sets a value indicating the member is approved + /// + public bool IsApproved { get; set; } + + /// + /// Gets or sets the alias of the member type + /// + public string MemberTypeAlias { get; set; } + + private static string UserIdToString(int userId) => string.Intern(userId.ToString()); + + // TODO: implement as per base identity user + //public bool LoginsChanged; + //public bool RolesChanged; + } +} diff --git a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs b/src/Umbraco.Infrastructure/Security/MembersUserStore.cs similarity index 79% rename from src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs rename to src/Umbraco.Infrastructure/Security/MembersUserStore.cs index 024511f8e4..79a2949a08 100644 --- a/src/Umbraco.Infrastructure/Members/UmbracoMembersUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MembersUserStore.cs @@ -5,26 +5,25 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Umbraco.Core; using Umbraco.Core.Mapping; -using Umbraco.Core.Members; using Umbraco.Core.Models; using Umbraco.Core.Scoping; using Umbraco.Core.Services; -namespace Umbraco.Infrastructure.Members +namespace Umbraco.Infrastructure.Security { /// /// A custom user store that uses Umbraco member data /// - public class UmbracoMembersUserStore : DisposableObjectSlim, + public class MembersUserStore : DisposableObjectSlim, //IUserStore, - IUserPasswordStore - //IUserEmailStore - //IUserLoginStore - //IUserRoleStore, - //IUserSecurityStampStore - //IUserLockoutStore - //IUserTwoFactorStore - //IUserSessionStore + IUserPasswordStore + //IUserEmailStore + //IUserLoginStore + //IUserRoleStore, + //IUserSecurityStampStore + //IUserLockoutStore + //IUserTwoFactorStore + //IUserSessionStore { private readonly bool _disposed = false; private readonly IMemberService _memberService; @@ -32,12 +31,12 @@ namespace Umbraco.Infrastructure.Members private readonly IScopeProvider _scopeProvider; /// - /// Initializes a new instance of the class for the members identity store + /// Initializes a new instance of the class for the members identity store /// /// The member service /// The mapper for properties /// The scope provider - public UmbracoMembersUserStore(IMemberService memberService, UmbracoMapper mapper, IScopeProvider scopeProvider) + public MembersUserStore(IMemberService memberService, UmbracoMapper mapper, IScopeProvider scopeProvider) { _memberService = memberService; _mapper = mapper; @@ -47,10 +46,10 @@ namespace Umbraco.Infrastructure.Members /// /// Create the member as an identity user /// - /// The identity user` for a member + /// The identity user for a member /// The cancellation token /// The identity result - public Task CreateAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) + public Task CreateAsync(MembersIdentityUser user, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -62,23 +61,23 @@ namespace Umbraco.Infrastructure.Members // create member // TODO: are we keeping this method, e.g. the Member Service? // The user service creates it directly, but this way we get the member type by alias first - IMember member = _memberService.CreateMember( + IMember memberEntity = _memberService.CreateMember( user.UserName, user.Email, user.Name.IsNullOrWhiteSpace() ? user.UserName : user.Name, user.MemberTypeAlias.IsNullOrWhiteSpace() ? Constants.Security.DefaultMemberTypeAlias : user.MemberTypeAlias); - UpdateMemberProperties(member, user); + UpdateMemberProperties(memberEntity, user); // TODO: do we want to accept empty passwords here - if third-party for example? // In other method if so? - _memberService.Save(member); + _memberService.Save(memberEntity); // re-assign id - user.Id = member.Id; + user.Id = UserIdToString(memberEntity.Id); - // TODO: [from backofficeuser] we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. - //bool isLoginsPropertyDirty = member.IsPropertyDirty(nameof(UmbracoMembersIdentityUser.Logins)); + // [from backofficeuser] we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. + bool isLoginsPropertyDirty = memberEntity.IsPropertyDirty(nameof(MembersIdentityUser.Logins)); //if (isLoginsPropertyDirty) //{ @@ -90,7 +89,7 @@ namespace Umbraco.Infrastructure.Members // x.UserData))); //} - if (!member.HasIdentity) + if (!memberEntity.HasIdentity) { throw new DataException("Could not create the member, check logs for details"); } @@ -121,19 +120,19 @@ namespace Umbraco.Infrastructure.Members //} } - private bool UpdateMemberProperties(IMember member, UmbracoMembersIdentityUser memberIdentityUser) + private bool UpdateMemberProperties(IMember member, MembersIdentityUser memberIdentityUser) { var anythingChanged = false; // don't assign anything if nothing has changed as this will trigger the track changes of the model - if (memberIdentityUser.IsPropertyDirty(nameof(UmbracoMembersIdentityUser.Name)) && + if (memberIdentityUser.IsPropertyDirty(nameof(MembersIdentityUser.Name)) && member.Name != memberIdentityUser.Name && memberIdentityUser.Name.IsNullOrWhiteSpace() == false) { anythingChanged = true; member.Name = memberIdentityUser.Name; } - if (memberIdentityUser.IsPropertyDirty(nameof(UmbracoMembersIdentityUser.Email)) && + if (memberIdentityUser.IsPropertyDirty(nameof(MembersIdentityUser.Email)) && member.Email != memberIdentityUser.Email && memberIdentityUser.Email.IsNullOrWhiteSpace() == false) { anythingChanged = true; @@ -152,14 +151,14 @@ namespace Umbraco.Infrastructure.Members } } - if (memberIdentityUser.IsPropertyDirty(nameof(UmbracoMembersIdentityUser.UserName)) && + if (memberIdentityUser.IsPropertyDirty(nameof(MembersIdentityUser.UserName)) && member.Username != memberIdentityUser.UserName && memberIdentityUser.UserName.IsNullOrWhiteSpace() == false) { anythingChanged = true; member.Username = memberIdentityUser.UserName; } - - if (memberIdentityUser.IsPropertyDirty(nameof(UmbracoMembersIdentityUser.PasswordHash)) + + if (memberIdentityUser.IsPropertyDirty(nameof(MembersIdentityUser.PasswordHash)) && member.RawPasswordValue != memberIdentityUser.PasswordHash && memberIdentityUser.PasswordHash.IsNullOrWhiteSpace() == false) { anythingChanged = true; @@ -204,17 +203,17 @@ namespace Umbraco.Infrastructure.Members return anythingChanged; } - public Task DeleteAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) + public Task DeleteAsync(MembersIdentityUser user, CancellationToken cancellationToken) { throw new NotImplementedException(); } - public Task FindByIdAsync(string userId, CancellationToken cancellationToken) + public Task FindByIdAsync(string userId, CancellationToken cancellationToken) { throw new NotImplementedException(); } - public async Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) + public async Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) { // TODO: confirm logic cancellationToken.ThrowIfCancellationRequested(); @@ -226,17 +225,17 @@ namespace Umbraco.Infrastructure.Members return null; } - UmbracoMembersIdentityUser result = _mapper.Map(member); + MembersIdentityUser result = _mapper.Map(member); return await Task.FromResult(result); } - public Task GetNormalizedUserNameAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) + public Task GetNormalizedUserNameAsync(MembersIdentityUser user, CancellationToken cancellationToken) { throw new NotImplementedException(); } - public Task GetUserIdAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) + public Task GetUserIdAsync(MembersIdentityUser user, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -248,7 +247,7 @@ namespace Umbraco.Infrastructure.Members return Task.FromResult(user.Id.ToString()); } - public Task GetUserNameAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken) + public Task GetUserNameAsync(MembersIdentityUser user, CancellationToken cancellationToken) { // TODO: unit tests for and implement all bodies cancellationToken.ThrowIfCancellationRequested(); @@ -268,7 +267,7 @@ namespace Umbraco.Infrastructure.Members /// The normalized member name /// The cancellation token /// A task once complete - public Task SetNormalizedUserNameAsync(UmbracoMembersIdentityUser user, string normalizedName, CancellationToken cancellationToken) => SetUserNameAsync(user, normalizedName, cancellationToken); + public Task SetNormalizedUserNameAsync(MembersIdentityUser user, string normalizedName, CancellationToken cancellationToken) => SetUserNameAsync(user, normalizedName, cancellationToken); /// /// Sets the user name as an async operation @@ -277,7 +276,7 @@ namespace Umbraco.Infrastructure.Members /// The member user name /// The cancellation token /// A task once complete - public Task SetUserNameAsync(UmbracoMembersIdentityUser user, string userName, CancellationToken cancellationToken) + public Task SetUserNameAsync(MembersIdentityUser user, string userName, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -296,7 +295,7 @@ namespace Umbraco.Infrastructure.Members /// The member identity user /// The cancellation token /// An identity result task - public Task UpdateAsync(UmbracoMembersIdentityUser member, CancellationToken cancellationToken) + public Task UpdateAsync(MembersIdentityUser member, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -358,7 +357,7 @@ namespace Umbraco.Infrastructure.Members /// The cancellation token /// Throws if the properties are null /// Returns asynchronously - public Task SetPasswordHashAsync(UmbracoMembersIdentityUser user, string passwordHash, CancellationToken cancellationToken = default(CancellationToken)) + public Task SetPasswordHashAsync(MembersIdentityUser user, string passwordHash, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -392,7 +391,7 @@ namespace Umbraco.Infrastructure.Members /// /// /// - public Task GetPasswordHashAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + public Task GetPasswordHashAsync(MembersIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -410,7 +409,7 @@ namespace Umbraco.Infrastructure.Members /// The identity user /// The cancellation token /// True if the user has a password - public Task HasPasswordAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + public Task HasPasswordAsync(MembersIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -421,5 +420,6 @@ namespace Umbraco.Infrastructure.Members return Task.FromResult(string.IsNullOrEmpty(user.PasswordHash) == false); } + private static string UserIdToString(int userId) => string.Intern(userId.ToString()); } } diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index b9368da89b..373d7df0e0 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -102,4 +102,8 @@ + + + + diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoMembersServiceCollectionExtensionsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoMembersServiceCollectionExtensionsTests.cs index 0432ffa7db..691d3e0e29 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoMembersServiceCollectionExtensionsTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoMembersServiceCollectionExtensionsTests.cs @@ -1,10 +1,9 @@ -using System; +using System; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; -using Umbraco.Core.Members; using Umbraco.Extensions; -using Umbraco.Infrastructure.Members; +using Umbraco.Infrastructure.Security; using Umbraco.Tests.Integration.Testing; namespace Umbraco.Tests.Integration.Umbraco.Web.BackOffice @@ -13,27 +12,18 @@ namespace Umbraco.Tests.Integration.Umbraco.Web.BackOffice public class UmbracoMembersServiceCollectionExtensionsTests : UmbracoIntegrationTest { [Test] - public void AddUmbracoMembersIdentity_ExpectUmbracoMembersUserStoreResolvable() + public void AddUmbracoMembersIdentity_ExpectMembersUserStoreResolvable() { - var userStore = Services.GetService>(); + IUserStore userStore = Services.GetService>(); Assert.IsNotNull(userStore); - Assert.AreEqual(typeof(UmbracoMembersUserStore), userStore.GetType()); + Assert.AreEqual(typeof(MembersUserStore), userStore.GetType()); } - //[Test] - //public void AddUmbracoMembersIdentity_ExpectUmbracoMembersClaimsPrincipalFactoryResolvable() - //{ - // var principalFactory = Services.GetService>(); - - // Assert.IsNotNull(principalFactory); - // Assert.AreEqual(typeof(UmbracoMembersClaimsPrincipalFactory), principalFactory.GetType()); - //} - [Test] - public void AddUmbracoMembersIdentity_ExpectUmbracomMembersUserManagerResolvable() + public void AddUmbracoMembersIdentity_ExpectUmbracoMembersUserManagerResolvable() { - var userManager = Services.GetService(); + IMembersUserManager userManager = Services.GetService(); Assert.NotNull(userManager); } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/UmbracoMemberIdentityUserManagerTests.cs similarity index 65% rename from src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserManagerTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/UmbracoMemberIdentityUserManagerTests.cs index 0645590b38..c5aea4d1a3 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserManagerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/UmbracoMemberIdentityUserManagerTests.cs @@ -1,52 +1,55 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; using Umbraco.Core.Configuration.Models; -using Umbraco.Core.Members; -using Umbraco.Infrastructure.Members; +using Umbraco.Core.Security; +using Umbraco.Infrastructure.Security; +using Umbraco.Net; +using Umbraco.Web.Common.Security; -namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Members +namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Security { [TestFixture] public class UmbracoMemberIdentityUserManagerTests { - private Mock> _mockMemberStore; - private Mock> _mockIdentityOptions; - private Mock> _mockPasswordHasher; - private Mock> _mockUserValidators; - private Mock>> _mockPasswordValidators; + private Mock> _mockMemberStore; + private Mock> _mockIdentityOptions; + private Mock> _mockPasswordHasher; + private Mock> _mockUserValidators; + private Mock>> _mockPasswordValidators; private Mock _mockNormalizer; private IdentityErrorDescriber _mockErrorDescriber; private Mock _mockServiceProviders; - private Mock>> _mockLogger; + private Mock>> _mockLogger; private Mock> _mockPasswordConfiguration; - public UmbracoMembersUserManager CreateSut() + public MembersUserManager CreateSut() { - _mockMemberStore = new Mock>(); - _mockIdentityOptions = new Mock>(); + _mockMemberStore = new Mock>(); + _mockIdentityOptions = new Mock>(); - var idOptions = new UmbracoMembersIdentityOptions { Lockout = { AllowedForNewUsers = false } }; + var idOptions = new MembersIdentityOptions { Lockout = { AllowedForNewUsers = false } }; _mockIdentityOptions.Setup(o => o.Value).Returns(idOptions); - _mockPasswordHasher = new Mock>(); + _mockPasswordHasher = new Mock>(); - var userValidators = new List>(); - _mockUserValidators = new Mock>(); - var validator = new Mock>(); + var userValidators = new List>(); + _mockUserValidators = new Mock>(); + var validator = new Mock>(); userValidators.Add(validator.Object); - _mockPasswordValidators = new Mock>>(); + _mockPasswordValidators = new Mock>>(); _mockNormalizer = new Mock(); _mockErrorDescriber = new IdentityErrorDescriber(); _mockServiceProviders = new Mock(); - _mockLogger = new Mock>>(); + _mockLogger = new Mock>>(); _mockPasswordConfiguration = new Mock>(); _mockPasswordConfiguration.Setup(x => x.Value).Returns(() => new MemberPasswordConfigurationSettings() @@ -54,26 +57,28 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Members }); - var pwdValidators = new List> + var pwdValidators = new List> { - new PasswordValidator() + new PasswordValidator() }; - var userManager = new UmbracoMembersUserManager( + var userManager = new MembersUserManager( + new Mock().Object, _mockMemberStore.Object, _mockIdentityOptions.Object, _mockPasswordHasher.Object, userValidators, pwdValidators, - new UpperInvariantLookupNormalizer(), - new IdentityErrorDescriber(), + new BackOfficeLookupNormalizer(), + new BackOfficeIdentityErrorDescriber(), _mockServiceProviders.Object, - new Mock>>().Object, + new Mock().Object, + new Mock>>().Object, _mockPasswordConfiguration.Object); validator.Setup(v => v.ValidateAsync( userManager, - It.IsAny())) + It.IsAny())) .Returns(Task.FromResult(IdentityResult.Success)).Verifiable(); return userManager; @@ -83,8 +88,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Members public async Task GivenICreateUser_AndTheIdentityResultFailed_ThenIShouldGetAFailedResultAsync() { //arrange - UmbracoMembersUserManager sut = CreateSut(); - UmbracoMembersIdentityUser fakeUser = new UmbracoMembersIdentityUser() + MembersUserManager sut = CreateSut(); + MembersIdentityUser fakeUser = new MembersIdentityUser() { PasswordConfig = "testConfig" }; @@ -116,7 +121,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Members public async Task GivenICreateUser_AndTheUserIsNull_ThenIShouldGetAFailedResultAsync() { //arrange - UmbracoMembersUserManager sut = CreateSut(); + MembersUserManager sut = CreateSut(); CancellationToken fakeCancellationToken = new CancellationToken() { }; IdentityError[] identityErrors = { @@ -144,8 +149,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Members public async Task GivenICreateANewUser_AndTheUserIsPopulatedCorrectly_ThenIShouldGetASuccessResultAsync() { //arrange - UmbracoMembersUserManager sut = CreateSut(); - UmbracoMembersIdentityUser fakeUser = new UmbracoMembersIdentityUser() + MembersUserManager sut = CreateSut(); + MembersIdentityUser fakeUser = new MembersIdentityUser() { PasswordConfig = "testConfig" }; diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/UmbracoMemberIdentityUserStoreTests.cs similarity index 88% rename from src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserStoreTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/UmbracoMemberIdentityUserStoreTests.cs index 5f8a143329..c1a39e7b57 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Members/UmbracoMemberIdentityUserStoreTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/UmbracoMemberIdentityUserStoreTests.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -8,11 +7,10 @@ using Microsoft.AspNetCore.Identity; using Moq; using NUnit.Framework; using Umbraco.Core.Mapping; -using Umbraco.Core.Members; using Umbraco.Core.Models; using Umbraco.Core.Scoping; using Umbraco.Core.Services; -using Umbraco.Infrastructure.Members; +using Umbraco.Infrastructure.Security; using Umbraco.Tests.UnitTests.Umbraco.Core.ShortStringHelper; namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Members @@ -22,10 +20,10 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Members { private Mock _mockMemberService; - public UmbracoMembersUserStore CreateSut() + public MembersUserStore CreateSut() { _mockMemberService = new Mock(); - return new UmbracoMembersUserStore( + return new MembersUserStore( _mockMemberService.Object, new UmbracoMapper(new MapDefinitionCollection(new List())), new Mock().Object); @@ -35,7 +33,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Members public void GivenICreateUser_AndTheUserIsNull_ThenIShouldGetAFailedResultAsync() { //arrange - UmbracoMembersUserStore sut = CreateSut(); + MembersUserStore sut = CreateSut(); CancellationToken fakeCancellationToken = new CancellationToken(){}; //act @@ -50,8 +48,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Members public async Task GivenICreateANewUser_AndTheUserIsPopulatedCorrectly_ThenIShouldGetASuccessResultAsync() { //arrange - UmbracoMembersUserStore sut = CreateSut(); - UmbracoMembersIdentityUser fakeUser = new UmbracoMembersIdentityUser() { }; + MembersUserStore sut = CreateSut(); + MembersIdentityUser fakeUser = new MembersIdentityUser() { }; CancellationToken fakeCancellationToken = new CancellationToken() { }; IMemberType fakeMemberType = new MemberType(new MockShortStringHelper(), 77); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs index 9924084bca..aa8fea1d78 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs @@ -11,14 +11,13 @@ using Umbraco.Core.Cache; using Umbraco.Core.Dictionary; using Umbraco.Core.Events; using Umbraco.Core.Mapping; -using Umbraco.Core.Members; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Security; using Umbraco.Core.Serialization; using Umbraco.Core.Services; using Umbraco.Core.Strings; -using Umbraco.Infrastructure.Members; +using Umbraco.Infrastructure.Security; using Umbraco.Tests.UnitTests.AutoFixture; using Umbraco.Tests.UnitTests.Umbraco.Core.ShortStringHelper; using Umbraco.Web; @@ -72,7 +71,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers [Test] [AutoMoqData] public async Task PostSaveMember_SaveNew_WhenAllIsSetupCorrectly_ExpectSuccessResponse( - [Frozen] IUmbracoMembersUserManager umbracoMembersUserManager, + [Frozen] IMembersUserManager umbracoMembersUserManager, IMemberTypeService memberTypeService, IDataTypeService dataTypeService, IMemberService memberService, @@ -101,7 +100,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers [Test] [AutoMoqData] public async Task PostSaveMember_Save_WhenAllIsSetupCorrectly_ExpectSuccessResponse( - [Frozen] IUmbracoMembersUserManager umbracoMembersUserManager, + [Frozen] IMembersUserManager umbracoMembersUserManager, IMemberTypeService memberTypeService, IDataTypeService dataTypeService, IMemberService memberService, @@ -130,7 +129,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers [Test] [AutoMoqData] public async Task PostSaveMember_SaveNew_WhenMemberEmailAlreadyExists_ExpectSuccessResponse( - [Frozen] IUmbracoMembersUserManager umbracoMembersUserManager, + [Frozen] IMembersUserManager umbracoMembersUserManager, IMemberTypeService memberTypeService, IDataTypeService dataTypeService, IMemberService memberService, @@ -159,7 +158,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers /// /// Setup all standard member data for test /// - private Member SetupMemberTestData(IUmbracoMembersUserManager umbracoMembersUserManager, + private Member SetupMemberTestData(IMembersUserManager umbracoMembersUserManager, IMemberTypeService memberTypeService, MapDefinitionCollection memberMapDefinition, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, @@ -170,7 +169,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers ContentSaveAction contentAction) { Mock.Get(umbracoMembersUserManager) - .Setup(x => x.CreateAsync(It.IsAny(), It.IsAny())) + .Setup(x => x.CreateAsync(It.IsAny())) .ReturnsAsync(() => IdentityResult.Success); Mock.Get(memberTypeService).Setup(x => x.GetDefault()).Returns("fakeAlias"); Mock.Get(backOfficeSecurityAccessor).Setup(x => x.BackOfficeSecurity).Returns(backOfficeSecurity); @@ -205,7 +204,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers UmbracoMapper mapper, IMemberService memberService, IMemberTypeService memberTypeService, - IUmbracoMembersUserManager membersUserManager, + IMembersUserManager membersUserManager, IDataTypeService dataTypeService, PropertyEditorCollection propertyEditorCollection, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) => diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index ec0ea75986..b3f6e528cf 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -25,7 +25,7 @@ using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; using Umbraco.Core.Strings; using Umbraco.Extensions; -using Umbraco.Infrastructure.Members; +using Umbraco.Infrastructure.Security; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.BackOffice.ModelBinders; using Umbraco.Web.Common.Attributes; @@ -35,7 +35,6 @@ using Umbraco.Web.Common.Filters; using Umbraco.Web.ContentApps; using Umbraco.Web.Models.ContentEditing; using Constants = Umbraco.Core.Constants; -using UmbracoMembersIdentityUser = Umbraco.Core.Members.UmbracoMembersIdentityUser; namespace Umbraco.Web.BackOffice.Controllers { @@ -52,7 +51,7 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly UmbracoMapper _umbracoMapper; private readonly IMemberService _memberService; private readonly IMemberTypeService _memberTypeService; - private readonly IUmbracoMembersUserManager _memberManager; + private readonly IMembersUserManager _memberManager; private readonly IDataTypeService _dataTypeService; private readonly ILocalizedTextService _localizedTextService; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; @@ -84,7 +83,7 @@ namespace Umbraco.Web.BackOffice.Controllers UmbracoMapper umbracoMapper, IMemberService memberService, IMemberTypeService memberTypeService, - IUmbracoMembersUserManager memberManager, + IMembersUserManager memberManager, IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IJsonSerializer jsonSerializer) @@ -243,10 +242,11 @@ namespace Umbraco.Web.BackOffice.Controllers throw new ArgumentNullException(nameof(contentItem)); } - if (ModelState.IsValid == false) - { - throw new HttpResponseException(HttpStatusCode.BadRequest, ModelState); - } + // TODO: this causes an issue when trying to correct an invalid model + //if (ModelState.IsValid == false) + //{ + // throw new HttpResponseException(HttpStatusCode.BadRequest, ModelState); + //} // If we've reached here it means: // * Our model has been bound @@ -268,19 +268,6 @@ namespace Umbraco.Web.BackOffice.Controllers throw HttpResponseException.CreateValidationErrorResponse(forDisplay); } - IMemberType memberType = _memberTypeService.Get(contentItem.ContentTypeAlias); - if (memberType == null) - { - throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}"); - } - - // Create the member with the MemberManager - var identityMember = UmbracoMembersIdentityUser.CreateNew( - contentItem.Username, - contentItem.Email, - memberType.Alias, - contentItem.Name); - // We're gonna look up the current roles now because the below code can cause // events to be raised and developers could be manually adding roles to members in // their handlers. If we don't look this up now there's a chance we'll just end up @@ -296,10 +283,10 @@ namespace Umbraco.Web.BackOffice.Controllers switch (contentItem.Action) { case ContentSaveAction.Save: - UpdateMemberData(contentItem); + await UpdateMemberDataAsync(contentItem); break; case ContentSaveAction.SaveNew: - IdentityResult identityResult = await CreateMemberAsync(contentItem, identityMember); + await CreateMemberAsync(contentItem); break; default: // we don't support anything else for members @@ -378,11 +365,24 @@ namespace Umbraco.Web.BackOffice.Controllers /// All member password processing and creation is done via the identity manager /// /// Member content data - /// The identity member to update /// The identity result of the created member - private async Task CreateMemberAsync(MemberSave contentItem, UmbracoMembersIdentityUser identityMember) + private async Task CreateMemberAsync(MemberSave contentItem) { + IMemberType memberType = _memberTypeService.Get(contentItem.ContentTypeAlias); + if (memberType == null) + { + throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}"); + } + + // Create the member with the MemberManager + var identityMember = MembersIdentityUser.CreateNew( + contentItem.Username, + contentItem.Email, + memberType.Alias, + contentItem.Name); + IdentityResult created = await _memberManager.CreateAsync(identityMember, contentItem.Password.NewPassword); + if (created.Succeeded == false) { throw HttpResponseException.CreateNotificationValidationErrorResponse(created.Errors.ToErrorMessage()); @@ -406,6 +406,73 @@ namespace Umbraco.Web.BackOffice.Controllers return created; } + /// + /// Update the member security data + /// If the password has been reset then this method will return the reset/generated password, otherwise will return null. + /// + /// The member to save + private async Task UpdateMemberDataAsync(MemberSave contentItem) + { + MembersIdentityUser identityMember = await _memberManager.FindByIdAsync(((int)contentItem.Id).ToString()); + if (identityMember == null) + { + } + + IdentityResult updatedResult = await _memberManager.UpdateAsync(identityMember); + + if (updatedResult.Succeeded == false) + { + throw HttpResponseException.CreateNotificationValidationErrorResponse(updatedResult.Errors.ToErrorMessage()); + } + + contentItem.PersistedContent.WriterId = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id; + + // If the user doesn't have access to sensitive values, then we need to check if any of the built in member property types + // have been marked as sensitive. If that is the case we cannot change these persisted values no matter what value has been posted. + // There's only 3 special ones we need to deal with that are part of the MemberSave instance: Comments, IsApproved, IsLockedOut + // but we will take care of this in a generic way below so that it works for all props. + if (!_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.HasAccessToSensitiveData()) + { + IMemberType memberType = _memberTypeService.Get(contentItem.PersistedContent.ContentTypeId); + var sensitiveProperties = memberType + .PropertyTypes.Where(x => memberType.IsSensitiveProperty(x.Alias)) + .ToList(); + + foreach (IPropertyType sensitiveProperty in sensitiveProperties) + { + ContentPropertyBasic destProp = contentItem.Properties.FirstOrDefault(x => x.Alias == sensitiveProperty.Alias); + if (destProp != null) + { + // if found, change the value of the contentItem model to the persisted value so it remains unchanged + object origValue = contentItem.PersistedContent.GetValue(sensitiveProperty.Alias); + destProp.Value = origValue; + } + } + } + + bool isLockedOut = contentItem.IsLockedOut; + + // if they were locked but now they are trying to be unlocked + if (contentItem.PersistedContent.IsLockedOut && isLockedOut == false) + { + contentItem.PersistedContent.IsLockedOut = false; + contentItem.PersistedContent.FailedPasswordAttempts = 0; + } + else if (!contentItem.PersistedContent.IsLockedOut && isLockedOut) + { + // NOTE: This should not ever happen unless someone is mucking around with the request data. + // An admin cannot simply lock a user, they get locked out by password attempts, but an admin can un-approve them + ModelState.AddModelError("custom", "An admin cannot lock a user"); + } + + // no password changes then exit ? + if (contentItem.Password != null) + { + // TODO: set the password using Identity core + contentItem.PersistedContent.RawPasswordValue = _memberManager.GeneratePassword(); + } + } + private void ValidateMemberData(MemberSave contentItem) { if (contentItem.Name.IsNullOrWhiteSpace()) @@ -433,13 +500,14 @@ namespace Umbraco.Web.BackOffice.Controllers if (contentItem.Password != null && !contentItem.Password.NewPassword.IsNullOrWhiteSpace()) { - Task> result = _memberManager.ValidatePassword(contentItem.Password.NewPassword); - if (result.Result.Exists(x => x.Succeeded == false)) - { - ModelState.AddPropertyError( - new ValidationResult($"Invalid password: {MapErrors(result.Result)}", new[] { "value" }), - $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password"); - } + //TODO: should we validate the password here, in advance? or when saving the identity user + //Task> result = _memberManager.ValidatePasswordAsync(contentItem.Password.NewPassword); + //if (result.Result.Exists(x => x.Succeeded == false)) + //{ + // ModelState.AddPropertyError( + // new ValidationResult($"Invalid password: {MapErrors(result.Result)}", new[] { "value" }), + // $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password"); + //} } } @@ -456,61 +524,6 @@ namespace Umbraco.Web.BackOffice.Controllers return sb.ToString(); } - /// - /// Update the member security data - /// If the password has been reset then this method will return the reset/generated password, otherwise will return null. - /// - /// The member to save - private void UpdateMemberData(MemberSave memberSave) - { - memberSave.PersistedContent.WriterId = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id; - - // If the user doesn't have access to sensitive values, then we need to check if any of the built in member property types - // have been marked as sensitive. If that is the case we cannot change these persisted values no matter what value has been posted. - // There's only 3 special ones we need to deal with that are part of the MemberSave instance: Comments, IsApproved, IsLockedOut - // but we will take care of this in a generic way below so that it works for all props. - if (!_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.HasAccessToSensitiveData()) - { - IMemberType memberType = _memberTypeService.Get(memberSave.PersistedContent.ContentTypeId); - var sensitiveProperties = memberType - .PropertyTypes.Where(x => memberType.IsSensitiveProperty(x.Alias)) - .ToList(); - - foreach (IPropertyType sensitiveProperty in sensitiveProperties) - { - ContentPropertyBasic destProp = memberSave.Properties.FirstOrDefault(x => x.Alias == sensitiveProperty.Alias); - if (destProp != null) - { - // if found, change the value of the contentItem model to the persisted value so it remains unchanged - object origValue = memberSave.PersistedContent.GetValue(sensitiveProperty.Alias); - destProp.Value = origValue; - } - } - } - - var isLockedOut = memberSave.IsLockedOut; - - // if they were locked but now they are trying to be unlocked - if (memberSave.PersistedContent.IsLockedOut && isLockedOut == false) - { - memberSave.PersistedContent.IsLockedOut = false; - memberSave.PersistedContent.FailedPasswordAttempts = 0; - } - else if (!memberSave.PersistedContent.IsLockedOut && isLockedOut) - { - // NOTE: This should not ever happen unless someone is mucking around with the request data. - // An admin cannot simply lock a user, they get locked out by password attempts, but an admin can un-approve them - ModelState.AddModelError("custom", "An admin cannot lock a user"); - } - - // no password changes then exit ? - if (memberSave.Password != null) - { - // TODO: set the password - memberSave.PersistedContent.RawPasswordValue = _memberManager.GeneratePassword(); - } - } - /// /// Permanently deletes a member /// diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoMemberIdentityBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoMemberIdentityBuilderExtensions.cs index bc8d2bcbfb..888d963891 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoMemberIdentityBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoMemberIdentityBuilderExtensions.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; -using Umbraco.Core.Members; +using Umbraco.Infrastructure.Security; namespace Umbraco.Extensions { @@ -13,7 +13,7 @@ namespace Umbraco.Extensions /// The type of the user manager to add. /// /// The current instance. - public static IdentityBuilder AddUserManager(this IdentityBuilder identityBuilder) where TUserManager : UserManager, TInterface + public static IdentityBuilder AddUserManager(this IdentityBuilder identityBuilder) where TUserManager : UserManager, TInterface { identityBuilder.AddUserManager(); identityBuilder.Services.AddScoped(typeof(TInterface), typeof(TUserManager)); diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoMembersUserServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoMembersUserServiceCollectionExtensions.cs index 6687c9e5be..d898e217cd 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoMembersUserServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoMembersUserServiceCollectionExtensions.cs @@ -1,11 +1,11 @@ -using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Umbraco.Core.Members; using Umbraco.Core.Security; using Umbraco.Core.Serialization; -using Umbraco.Infrastructure.Members; +using Umbraco.Infrastructure.Security; using Umbraco.Web.BackOffice.Security; +using Umbraco.Web.Common.Security; namespace Umbraco.Extensions { @@ -19,17 +19,17 @@ namespace Umbraco.Extensions { services.BuildUmbracoMembersIdentity() .AddDefaultTokenProviders() - .AddUserStore() - .AddUserManager(); + .AddUserStore() + .AddUserManager(); } - private static UmbracoMembersIdentityBuilder BuildUmbracoMembersIdentity(this IServiceCollection services) + private static MembersIdentityBuilder BuildUmbracoMembersIdentity(this IServiceCollection services) { // Services used by Umbraco members identity - services.TryAddScoped, UserValidator>(); - services.TryAddScoped, PasswordValidator>(); - services.TryAddScoped, PasswordHasher>(); - return new UmbracoMembersIdentityBuilder(services); + services.TryAddScoped, UserValidator>(); + services.TryAddScoped, PasswordValidator>(); + services.TryAddScoped, PasswordHasher>(); + return new MembersIdentityBuilder(services); } } } diff --git a/src/Umbraco.Web.Common/Security/MembersUserManager.cs b/src/Umbraco.Web.Common/Security/MembersUserManager.cs new file mode 100644 index 0000000000..1a8c3e509a --- /dev/null +++ b/src/Umbraco.Web.Common/Security/MembersUserManager.cs @@ -0,0 +1,286 @@ +using System; +using System.Collections.Generic; +using System.Security.Principal; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Core; +using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Security; +using Umbraco.Extensions; +using Umbraco.Infrastructure.Security; +using Umbraco.Net; +using Umbraco.Web.Models.ContentEditing; + + +namespace Umbraco.Web.Common.Security +{ + public class MembersUserManager : UmbracoUserManager, IMembersUserManager + { + private readonly IHttpContextAccessor _httpContextAccessor; + + public MembersUserManager( + IIpResolver ipResolver, + IUserStore store, + IOptions optionsAccessor, + IPasswordHasher passwordHasher, + IEnumerable> userValidators, + IEnumerable> passwordValidators, + //TODO: do we need members versions of this? + BackOfficeLookupNormalizer keyNormalizer, + BackOfficeIdentityErrorDescriber errors, + IServiceProvider services, + IHttpContextAccessor httpContextAccessor, + ILogger> logger, + IOptions passwordConfiguration) + : base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger, passwordConfiguration) + { + _httpContextAccessor = httpContextAccessor; + } + + /// + /// Gets or sets the default members user password checker + /// + public IMembersUserPasswordChecker MembersUserPasswordChecker { get; set; } + // TODO: as per backoffice: This isn't a good way to set this, it needs to be injected + + /// + /// + /// By default this uses the standard ASP.Net Identity approach which is: + /// * Get password store + /// * Call VerifyPasswordAsync with the password store + user + password + /// * Uses the PasswordHasher.VerifyHashedPassword to compare the stored password + /// + /// In some cases people want simple custom control over the username/password check, for simplicity + /// sake, developers would like the users to simply validate against an LDAP directory but the user + /// data remains stored inside of Umbraco. + /// See: http://issues.umbraco.org/issue/U4-7032 for the use cases. + /// + /// We've allowed this check to be overridden with a simple callback so that developers don't actually + /// have to implement/override this class. + /// + public override async Task CheckPasswordAsync(MembersIdentityUser user, string password) + { + if (MembersUserPasswordChecker != null) + { + MembersUserPasswordCheckerResult result = await MembersUserPasswordChecker.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 != MembersUserPasswordCheckerResult.FallbackToDefaultChecker) + { + return result == MembersUserPasswordCheckerResult.ValidCredentials; + } + } + + // use the default behavior + return await base.CheckPasswordAsync(user, password); + } + + /// + /// Override to check the user approval value as well as the user lock out date, by default this only checks the user's locked out date + /// + /// The user + /// True if the user is locked out, else false + /// + /// In the ASP.NET Identity world, there is only one value for being locked out, in Umbraco we have 2 so when checking this for Umbraco we need to check both values + /// + public override async Task IsLockedOutAsync(MembersIdentityUser user) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (user.IsApproved == false) + { + return true; + } + + return await base.IsLockedOutAsync(user); + } + + public override async Task AccessFailedAsync(MembersIdentityUser user) + { + IdentityResult result = await base.AccessFailedAsync(user); + + // Slightly confusing: this will return a Success if we successfully update the AccessFailed count + if (result.Succeeded) + { + RaiseLoginFailedEvent(_httpContextAccessor.HttpContext?.User, user.Id); + } + + return result; + } + + public override async Task ChangePasswordWithResetAsync(string userId, string token, string newPassword) + { + IdentityResult result = await base.ChangePasswordWithResetAsync(userId, token, newPassword); + if (result.Succeeded) + { + RaisePasswordChangedEvent(_httpContextAccessor.HttpContext?.User, userId); + } + + return result; + } + + public override async Task ChangePasswordAsync(MembersIdentityUser user, string currentPassword, string newPassword) + { + IdentityResult result = await base.ChangePasswordAsync(user, currentPassword, newPassword); + if (result.Succeeded) + { + RaisePasswordChangedEvent(_httpContextAccessor.HttpContext?.User, user.Id); + } + + return result; + } + + /// + public override async Task SetLockoutEndDateAsync(MembersIdentityUser user, DateTimeOffset? lockoutEnd) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + IdentityResult result = await base.SetLockoutEndDateAsync(user, lockoutEnd); + + // The way we unlock is by setting the lockoutEnd date to the current datetime + if (result.Succeeded && lockoutEnd >= DateTimeOffset.UtcNow) + { + RaiseAccountLockedEvent(_httpContextAccessor.HttpContext?.User, user.Id); + } + else + { + RaiseAccountUnlockedEvent(_httpContextAccessor.HttpContext?.User, user.Id); + + // Resets the login attempt fails back to 0 when unlock is clicked + await ResetAccessFailedCountAsync(user); + } + + return result; + } + + /// + public override async Task ResetAccessFailedCountAsync(MembersIdentityUser user) + { + IdentityResult result = await base.ResetAccessFailedCountAsync(user); + + // raise the event now that it's reset + RaiseResetAccessFailedCountEvent(_httpContextAccessor.HttpContext?.User, user.Id); + + return result; + } + + private string GetCurrentUserId(IPrincipal currentUser) + { + UmbracoBackOfficeIdentity umbIdentity = currentUser?.GetUmbracoIdentity(); + var currentUserId = umbIdentity?.GetUserId() ?? Core.Constants.Security.SuperUserIdAsString; + return currentUserId; + } + + private IdentityAuditEventArgs CreateArgs(AuditEvent auditEvent, IPrincipal currentUser, string affectedUserId, string affectedUsername) + { + var currentUserId = GetCurrentUserId(currentUser); + var ip = IpResolver.GetCurrentRequestIpAddress(); + return new IdentityAuditEventArgs(auditEvent, ip, currentUserId, string.Empty, affectedUserId, affectedUsername); + } + + private IdentityAuditEventArgs CreateArgs(AuditEvent auditEvent, BackOfficeIdentityUser currentUser, string affectedUserId, string affectedUsername) + { + var currentUserId = currentUser.Id; + var ip = IpResolver.GetCurrentRequestIpAddress(); + return new IdentityAuditEventArgs(auditEvent, ip, currentUserId, string.Empty, affectedUserId, affectedUsername); + } + + // TODO: Review where these are raised and see if they can be simplified and either done in the this usermanager or the signin manager, + // lastly we'll resort to the authentication controller but we should try to remove all instances of that occuring + public void RaiseAccountLockedEvent(IPrincipal currentUser, string userId) => OnAccountLocked(CreateArgs(AuditEvent.AccountLocked, currentUser, userId, string.Empty)); + + public void RaiseAccountUnlockedEvent(IPrincipal currentUser, string userId) => OnAccountUnlocked(CreateArgs(AuditEvent.AccountUnlocked, currentUser, userId, string.Empty)); + + public void RaiseForgotPasswordRequestedEvent(IPrincipal currentUser, string userId) => OnForgotPasswordRequested(CreateArgs(AuditEvent.ForgotPasswordRequested, currentUser, userId, string.Empty)); + + public void RaiseForgotPasswordChangedSuccessEvent(IPrincipal currentUser, string userId) => OnForgotPasswordChangedSuccess(CreateArgs(AuditEvent.ForgotPasswordChangedSuccess, currentUser, userId, string.Empty)); + + public void RaiseLoginFailedEvent(IPrincipal currentUser, string userId) => OnLoginFailed(CreateArgs(AuditEvent.LoginFailed, currentUser, userId, string.Empty)); + + public void RaiseLoginRequiresVerificationEvent(IPrincipal currentUser, string userId) => OnLoginRequiresVerification(CreateArgs(AuditEvent.LoginRequiresVerification, currentUser, userId, string.Empty)); + + public void RaiseLoginSuccessEvent(IPrincipal currentUser, string userId) => OnLoginSuccess(CreateArgs(AuditEvent.LoginSucces, currentUser, userId, string.Empty)); + + public SignOutAuditEventArgs RaiseLogoutSuccessEvent(IPrincipal currentUser, string userId) + { + var currentUserId = GetCurrentUserId(currentUser); + var args = new SignOutAuditEventArgs(AuditEvent.LogoutSuccess, IpResolver.GetCurrentRequestIpAddress(), performingUser: currentUserId, affectedUser: userId); + OnLogoutSuccess(args); + return args; + } + + public void RaisePasswordChangedEvent(IPrincipal currentUser, string userId) => OnPasswordChanged(CreateArgs(AuditEvent.LogoutSuccess, currentUser, userId, string.Empty)); + + public void RaiseResetAccessFailedCountEvent(IPrincipal currentUser, string userId) => OnResetAccessFailedCount(CreateArgs(AuditEvent.ResetAccessFailedCount, currentUser, userId, string.Empty)); + + public UserInviteEventArgs RaiseSendingUserInvite(IPrincipal currentUser, UserInvite invite, IUser createdUser) + { + var currentUserId = GetCurrentUserId(currentUser); + var ip = IpResolver.GetCurrentRequestIpAddress(); + var args = new UserInviteEventArgs(ip, currentUserId, invite, createdUser); + OnSendingUserInvite(args); + return args; + } + + public bool HasSendingUserInviteEventHandler => SendingUserInvite != null; + + // TODO: These static events are problematic. Moving forward we don't want static events at all but we cannot + // have non-static events here because the user manager is a Scoped instance not a singleton + // so we'll have to deal with this a diff way i.e. refactoring how events are done entirely + public static event EventHandler AccountLocked; + public static event EventHandler AccountUnlocked; + public static event EventHandler ForgotPasswordRequested; + public static event EventHandler ForgotPasswordChangedSuccess; + public static event EventHandler LoginFailed; + public static event EventHandler LoginRequiresVerification; + public static event EventHandler LoginSuccess; + public static event EventHandler LogoutSuccess; + public static event EventHandler PasswordChanged; + public static event EventHandler PasswordReset; + public static event EventHandler ResetAccessFailedCount; + + /// + /// Raised when a user is invited + /// + public static event EventHandler SendingUserInvite; // this event really has nothing to do with the user manager but was the most convenient place to put it + + protected virtual void OnAccountLocked(IdentityAuditEventArgs e) => AccountLocked?.Invoke(this, e); + + protected virtual void OnSendingUserInvite(UserInviteEventArgs e) => SendingUserInvite?.Invoke(this, e); + + protected virtual void OnAccountUnlocked(IdentityAuditEventArgs e) => AccountUnlocked?.Invoke(this, e); + + protected virtual void OnForgotPasswordRequested(IdentityAuditEventArgs e) => ForgotPasswordRequested?.Invoke(this, e); + + protected virtual void OnForgotPasswordChangedSuccess(IdentityAuditEventArgs e) => ForgotPasswordChangedSuccess?.Invoke(this, e); + + protected virtual void OnLoginFailed(IdentityAuditEventArgs e) => LoginFailed?.Invoke(this, e); + + protected virtual void OnLoginRequiresVerification(IdentityAuditEventArgs e) => LoginRequiresVerification?.Invoke(this, e); + + protected virtual void OnLoginSuccess(IdentityAuditEventArgs e) => LoginSuccess?.Invoke(this, e); + + protected virtual void OnLogoutSuccess(SignOutAuditEventArgs e) => LogoutSuccess?.Invoke(this, e); + + protected virtual void OnPasswordChanged(IdentityAuditEventArgs e) => PasswordChanged?.Invoke(this, e); + + protected virtual void OnPasswordReset(IdentityAuditEventArgs e) => PasswordReset?.Invoke(this, e); + + protected virtual void OnResetAccessFailedCount(IdentityAuditEventArgs e) => ResetAccessFailedCount?.Invoke(this, e); + } +} diff --git a/src/Umbraco.Web.UI.NetCore/appsettings.json b/src/Umbraco.Web.UI.NetCore/appsettings.json index 43ae07d5d6..29682186f6 100644 --- a/src/Umbraco.Web.UI.NetCore/appsettings.json +++ b/src/Umbraco.Web.UI.NetCore/appsettings.json @@ -1,6 +1,6 @@ { "ConnectionStrings": { - "umbracoDbDSN": "Server=(LocalDB)\\Umbraco;Database=NetCore;Integrated Security=true" + "umbracoDbDSN": "" }, "Serilog": { "MinimumLevel": { @@ -71,4 +71,4 @@ } } } -} \ No newline at end of file +} From f24dad1f30e035ad45a86492b05bbb8b279683eb Mon Sep 17 00:00:00 2001 From: emmagarland Date: Tue, 8 Dec 2020 02:07:27 +0000 Subject: [PATCH 21/72] More renaming, however if the password isn't validated then there will be an exception thrown in identity. --- ...s => MembersServiceCollectionExtensionsTests.cs} | 6 +++--- ...cs => MembersUserServiceCollectionExtensions.cs} | 13 +++++-------- .../Extensions/UmbracoBuilderExtensions.cs | 6 +++--- 3 files changed, 11 insertions(+), 14 deletions(-) rename src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/{UmbracoMembersServiceCollectionExtensionsTests.cs => MembersServiceCollectionExtensionsTests.cs} (77%) rename src/Umbraco.Web.BackOffice/Extensions/{UmbracoMembersUserServiceCollectionExtensions.cs => MembersUserServiceCollectionExtensions.cs} (67%) diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoMembersServiceCollectionExtensionsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/MembersServiceCollectionExtensionsTests.cs similarity index 77% rename from src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoMembersServiceCollectionExtensionsTests.cs rename to src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/MembersServiceCollectionExtensionsTests.cs index 691d3e0e29..85d1010b44 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoMembersServiceCollectionExtensionsTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/MembersServiceCollectionExtensionsTests.cs @@ -9,10 +9,10 @@ using Umbraco.Tests.Integration.Testing; namespace Umbraco.Tests.Integration.Umbraco.Web.BackOffice { [TestFixture] - public class UmbracoMembersServiceCollectionExtensionsTests : UmbracoIntegrationTest + public class MembersServiceCollectionExtensionsTests : UmbracoIntegrationTest { [Test] - public void AddUmbracoMembersIdentity_ExpectMembersUserStoreResolvable() + public void AddXMembersIdentity_ExpectMembersUserStoreResolvable() { IUserStore userStore = Services.GetService>(); @@ -21,7 +21,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Web.BackOffice } [Test] - public void AddUmbracoMembersIdentity_ExpectUmbracoMembersUserManagerResolvable() + public void AddMembersIdentity_ExpectMembersUserManagerResolvable() { IMembersUserManager userManager = Services.GetService(); diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoMembersUserServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/MembersUserServiceCollectionExtensions.cs similarity index 67% rename from src/Umbraco.Web.BackOffice/Extensions/UmbracoMembersUserServiceCollectionExtensions.cs rename to src/Umbraco.Web.BackOffice/Extensions/MembersUserServiceCollectionExtensions.cs index d898e217cd..62b4904838 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoMembersUserServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/MembersUserServiceCollectionExtensions.cs @@ -1,29 +1,26 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Umbraco.Core.Security; -using Umbraco.Core.Serialization; using Umbraco.Infrastructure.Security; -using Umbraco.Web.BackOffice.Security; using Umbraco.Web.Common.Security; namespace Umbraco.Extensions { - public static class UmbracoMembersUserServiceCollectionExtensions + public static class MembersUserServiceCollectionExtensions { /// - /// Adds the services required for using Umbraco Members Identity + /// Adds the services required for using Members Identity /// /// - public static void AddUmbracoMembersIdentity(this IServiceCollection services) + public static void AddMembersIdentity(this IServiceCollection services) { - services.BuildUmbracoMembersIdentity() + services.BuildMembersIdentity() .AddDefaultTokenProviders() .AddUserStore() .AddUserManager(); } - private static MembersIdentityBuilder BuildUmbracoMembersIdentity(this IServiceCollection services) + private static MembersIdentityBuilder BuildMembersIdentity(this IServiceCollection services) { // Services used by Umbraco members identity services.TryAddScoped, UserValidator>(); diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs index 137f5b7bc8..8d35239e2a 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs @@ -18,7 +18,7 @@ namespace Umbraco.Extensions .AddRuntimeMinifier() .AddBackOffice() .AddBackOfficeIdentity() - .AddUmbracoMembersIdentity() + .AddMembersIdentity() .AddBackOfficeAuthorizationPolicies() .AddMiniProfiler() .AddMvcAndRazor() @@ -54,9 +54,9 @@ namespace Umbraco.Extensions return builder; } - public static IUmbracoBuilder AddUmbracoMembersIdentity(this IUmbracoBuilder builder) + public static IUmbracoBuilder AddMembersIdentity(this IUmbracoBuilder builder) { - builder.Services.AddUmbracoMembersIdentity(); + builder.Services.AddMembersIdentity(); return builder; } From 2f7766a9722562f055c34ba59588286f709a675d Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Tue, 8 Dec 2020 11:25:01 +0000 Subject: [PATCH 22/72] Updated correct class, commented --- .../TestServerTest/UmbracoTestServerTestBase.cs | 4 ++-- .../MembersServiceCollectionExtensionsTests.cs | 2 +- src/Umbraco.Web.BackOffice/Controllers/MemberController.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index c4925104e5..6e2e139f69 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -1,4 +1,4 @@ - + using System; using System.Linq.Expressions; using System.Net.Http; @@ -144,7 +144,7 @@ namespace Umbraco.Tests.Integration.TestServerTest .AddRuntimeMinifier() .AddBackOffice() .AddBackOfficeIdentity() - .AddUmbracoMembersIdentity() + .AddMembersIdentity() .AddBackOfficeAuthorizationPolicies(TestAuthHandler.TestAuthenticationScheme) .AddPreviewSupport() //.WithMiniProfiler() // we don't want this running in tests diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/MembersServiceCollectionExtensionsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/MembersServiceCollectionExtensionsTests.cs index 85d1010b44..254bb8537f 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/MembersServiceCollectionExtensionsTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/MembersServiceCollectionExtensionsTests.cs @@ -28,6 +28,6 @@ namespace Umbraco.Tests.Integration.Umbraco.Web.BackOffice Assert.NotNull(userManager); } - protected override Action CustomTestSetup => (services) => services.AddUmbracoMembersIdentity(); + protected override Action CustomTestSetup => (services) => services.AddMembersIdentity(); } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index b3f6e528cf..e8cbc1114b 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -392,7 +392,7 @@ namespace Umbraco.Web.BackOffice.Controllers IMember member = _memberService.GetByEmail(contentItem.Email); member.CreatorId = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id; - // should this be removed since we've moved passwords out? + // TODO: should this be removed since we've moved passwords out? member.RawPasswordValue = identityMember.PasswordHash; member.Comments = contentItem.Comments; From 205edf57b739f30a5a884be86fdfbe7a92e7aa08 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Tue, 8 Dec 2020 17:18:22 +0000 Subject: [PATCH 23/72] Added store functionality based on backoffice user (to be revisited as a lot of it may be shareable) and also added a new members service method for membergroups. --- .../Services/IMembershipRoleService.cs | 4 +- .../Security/IMembersUserPasswordChecker.cs | 20 - .../Security/MembersUserStore.cs | 815 ++++++++++++------ .../Services/Implement/MemberService.cs | 14 + .../Services/MemberServiceTests.cs | 15 +- .../UmbracoMemberIdentityUserStoreTests.cs | 3 +- .../Controllers/MemberControllerUnitTests.cs | 34 +- .../Controllers/MemberController.cs | 35 +- .../Trees/MemberTreeController.cs | 2 +- .../Security/MembersUserManager.cs | 45 +- 10 files changed, 620 insertions(+), 367 deletions(-) delete mode 100644 src/Umbraco.Infrastructure/Security/IMembersUserPasswordChecker.cs diff --git a/src/Umbraco.Core/Services/IMembershipRoleService.cs b/src/Umbraco.Core/Services/IMembershipRoleService.cs index 7389bb9799..73479801d2 100644 --- a/src/Umbraco.Core/Services/IMembershipRoleService.cs +++ b/src/Umbraco.Core/Services/IMembershipRoleService.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.Querying; @@ -9,6 +10,7 @@ namespace Umbraco.Core.Services { void AddRole(string roleName); IEnumerable GetAllRoles(); + IEnumerable GetAllRolesTyped(); IEnumerable GetAllRoles(int memberId); IEnumerable GetAllRoles(string username); IEnumerable GetAllRolesIds(); diff --git a/src/Umbraco.Infrastructure/Security/IMembersUserPasswordChecker.cs b/src/Umbraco.Infrastructure/Security/IMembersUserPasswordChecker.cs deleted file mode 100644 index 969b4feb79..0000000000 --- a/src/Umbraco.Infrastructure/Security/IMembersUserPasswordChecker.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Threading.Tasks; -using Umbraco.Core.Security; - -namespace Umbraco.Infrastructure.Security -{ - /// - /// Used by the MembersUserManager to check the username/password which allows for developers to more easily - /// set the logic for this procedure. - /// - public interface IMembersUserPasswordChecker - { - /// - /// Checks a password for a member - /// - /// - /// TODO: what should our implementation be for members? - /// - Task CheckPasswordAsync(MembersIdentityUser user, string password); - } -} diff --git a/src/Umbraco.Infrastructure/Security/MembersUserStore.cs b/src/Umbraco.Infrastructure/Security/MembersUserStore.cs index 79a2949a08..70ca06a17a 100644 --- a/src/Umbraco.Infrastructure/Security/MembersUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MembersUserStore.cs @@ -1,11 +1,16 @@ using System; +using System.Collections.Generic; +using System.ComponentModel; using System.Data; +using System.Linq; +using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Umbraco.Core; using Umbraco.Core.Mapping; using Umbraco.Core.Models; +using Umbraco.Core.Models.Identity; using Umbraco.Core.Scoping; using Umbraco.Core.Services; @@ -14,18 +19,8 @@ namespace Umbraco.Infrastructure.Security /// /// A custom user store that uses Umbraco member data /// - public class MembersUserStore : DisposableObjectSlim, - //IUserStore, - IUserPasswordStore - //IUserEmailStore - //IUserLoginStore - //IUserRoleStore, - //IUserSecurityStampStore - //IUserLockoutStore - //IUserTwoFactorStore - //IUserSessionStore + public class MembersUserStore : UserStoreBase, string, IdentityUserClaim, IdentityUserRole, IdentityUserLogin, IdentityUserToken, IdentityRoleClaim> { - private readonly bool _disposed = false; private readonly IMemberService _memberService; private readonly UmbracoMapper _mapper; private readonly IScopeProvider _scopeProvider; @@ -36,20 +31,31 @@ namespace Umbraco.Infrastructure.Security /// The member service /// The mapper for properties /// The scope provider - public MembersUserStore(IMemberService memberService, UmbracoMapper mapper, IScopeProvider scopeProvider) + /// The error describer + /// + public MembersUserStore(IMemberService memberService, UmbracoMapper mapper, IScopeProvider scopeProvider, IdentityErrorDescriber describer) + : base(describer) { - _memberService = memberService; + _memberService = memberService ?? throw new ArgumentNullException(nameof(memberService)); _mapper = mapper; _scopeProvider = scopeProvider; } /// - /// Create the member as an identity user + /// Not supported in Umbraco /// - /// The identity user for a member - /// The cancellation token - /// The identity result - public Task CreateAsync(MembersIdentityUser user, CancellationToken cancellationToken) + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override IQueryable Users => throw new NotImplementedException(); + + /// + public override Task GetNormalizedUserNameAsync(MembersIdentityUser user, CancellationToken cancellationToken) => GetUserNameAsync(user, cancellationToken); + + /// + public override Task SetNormalizedUserNameAsync(MembersIdentityUser user, string normalizedName, CancellationToken cancellationToken) => SetUserNameAsync(user, normalizedName, cancellationToken); + + /// + public override Task CreateAsync(MembersIdentityUser user, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -67,18 +73,24 @@ namespace Umbraco.Infrastructure.Security user.Name.IsNullOrWhiteSpace() ? user.UserName : user.Name, user.MemberTypeAlias.IsNullOrWhiteSpace() ? Constants.Security.DefaultMemberTypeAlias : user.MemberTypeAlias); + // [from backofficeuser] we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. + var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(MembersIdentityUser.Logins)); + UpdateMemberProperties(memberEntity, user); // TODO: do we want to accept empty passwords here - if third-party for example? // In other method if so? _memberService.Save(memberEntity); + if (!memberEntity.HasIdentity) + { + throw new DataException("Could not create the member, check logs for details"); + } + // re-assign id user.Id = UserIdToString(memberEntity.Id); - // [from backofficeuser] we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. - bool isLoginsPropertyDirty = memberEntity.IsPropertyDirty(nameof(MembersIdentityUser.Logins)); - + //TODO: confirm re externallogins implementation //if (isLoginsPropertyDirty) //{ // _externalLoginService.Save( @@ -89,153 +101,13 @@ namespace Umbraco.Infrastructure.Security // x.UserData))); //} - if (!memberEntity.HasIdentity) - { - throw new DataException("Could not create the member, check logs for details"); - } - return Task.FromResult(IdentityResult.Success); - // TODO: confirm and implement - //if (memberUser.LoginsChanged) - //{ - // var logins = await GetLoginsAsync(memberUser); - // _externalLoginStore.SaveUserLogins(member.Id, logins); - //} - - // TODO: confirm and implement - //if (memberUser.RolesChanged) - //{ - //IMembershipRoleService memberRoleService = _memberService; - - //var persistedRoles = memberRoleService.GetAllRoles(member.Id).ToArray(); - //var userRoles = memberUser.Roles.Select(x => x.RoleName).ToArray(); - - //var keep = persistedRoles.Intersect(userRoles).ToArray(); - //var remove = persistedRoles.Except(keep).ToArray(); - //var add = userRoles.Except(persistedRoles).ToArray(); - - //memberRoleService.DissociateRoles(new[] { member.Id }, remove); - //memberRoleService.AssignRoles(new[] { member.Id }, add); - //} + // TODO: confirm re roles implementations } - private bool UpdateMemberProperties(IMember member, MembersIdentityUser memberIdentityUser) - { - var anythingChanged = false; - - // don't assign anything if nothing has changed as this will trigger the track changes of the model - if (memberIdentityUser.IsPropertyDirty(nameof(MembersIdentityUser.Name)) && - member.Name != memberIdentityUser.Name && memberIdentityUser.Name.IsNullOrWhiteSpace() == false) - { - anythingChanged = true; - member.Name = memberIdentityUser.Name; - } - - if (memberIdentityUser.IsPropertyDirty(nameof(MembersIdentityUser.Email)) && - member.Email != memberIdentityUser.Email && memberIdentityUser.Email.IsNullOrWhiteSpace() == false) - { - anythingChanged = true; - member.Email = memberIdentityUser.Email; - } - - if (member.IsLockedOut != memberIdentityUser.IsLockedOut) - { - anythingChanged = true; - member.IsLockedOut = memberIdentityUser.IsLockedOut; - - if (member.IsLockedOut) - { - // need to set the last lockout date - member.LastLockoutDate = DateTime.Now; - } - } - - if (memberIdentityUser.IsPropertyDirty(nameof(MembersIdentityUser.UserName)) && - member.Username != memberIdentityUser.UserName && memberIdentityUser.UserName.IsNullOrWhiteSpace() == false) - { - anythingChanged = true; - member.Username = memberIdentityUser.UserName; - } - - if (memberIdentityUser.IsPropertyDirty(nameof(MembersIdentityUser.PasswordHash)) - && member.RawPasswordValue != memberIdentityUser.PasswordHash && memberIdentityUser.PasswordHash.IsNullOrWhiteSpace() == false) - { - anythingChanged = true; - member.RawPasswordValue = memberIdentityUser.PasswordHash; - member.PasswordConfiguration = memberIdentityUser.PasswordConfig; - } - - // TODO: Roles - // [Comment] Same comment as per BackOfficeUserStore: Fix this for Groups too - //if (identityUser.IsPropertyDirty(nameof(UmbracoMembersIdentityUser.Roles)) || identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Groups))) - //{ - // var userGroupAliases = member.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) - // { - // anythingChanged = true; - - // //clear out the current groups (need to ToArray since we are modifying the iterator) - // member.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) - // { - // member.AddGroup(group); - // } - - // //re-assign - // identityUser.Groups = groups; - // } - //} - - memberIdentityUser.ResetDirtyProperties(false); - return anythingChanged; - } - - public Task DeleteAsync(MembersIdentityUser user, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task FindByIdAsync(string userId, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public async Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) - { - // TODO: confirm logic - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - - IMember member = _memberService.GetByUsername(normalizedUserName); - if (member == null) - { - return null; - } - - MembersIdentityUser result = _mapper.Map(member); - - return await Task.FromResult(result); - } - - public Task GetNormalizedUserNameAsync(MembersIdentityUser user, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task GetUserIdAsync(MembersIdentityUser user, CancellationToken cancellationToken) + /// + public override Task UpdateAsync(MembersIdentityUser user, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -244,70 +116,10 @@ namespace Umbraco.Infrastructure.Security throw new ArgumentNullException(nameof(user)); } - return Task.FromResult(user.Id.ToString()); - } - - public Task GetUserNameAsync(MembersIdentityUser user, CancellationToken cancellationToken) - { - // TODO: unit tests for and implement all bodies - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return Task.FromResult(user.UserName); - } - - /// - /// Sets the normalized user name - /// - /// The member identity user - /// The normalized member name - /// The cancellation token - /// A task once complete - public Task SetNormalizedUserNameAsync(MembersIdentityUser user, string normalizedName, CancellationToken cancellationToken) => SetUserNameAsync(user, normalizedName, cancellationToken); - - /// - /// Sets the user name as an async operation - /// - /// The member identity user - /// The member user name - /// The cancellation token - /// A task once complete - public Task SetUserNameAsync(MembersIdentityUser user, string userName, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - user.UserName = userName; - return Task.CompletedTask; - } - - /// - /// Update the user asynchronously - /// - /// The member identity user - /// The cancellation token - /// An identity result task - public Task UpdateAsync(MembersIdentityUser member, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (member == null) - { - throw new ArgumentNullException(nameof(member)); - } - - Attempt asInt = member.Id.TryConvertTo(); + Attempt asInt = user.Id.TryConvertTo(); if (asInt == false) { - throw new InvalidOperationException("The member id must be an integer to work with the Umbraco"); + throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); } using (IScope scope = _scopeProvider.CreateScope()) @@ -316,18 +128,20 @@ namespace Umbraco.Infrastructure.Security if (found != null) { // we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. - // var isLoginsPropertyDirty = member.IsPropertyDirty(nameof(UmbracoMembersIdentityUser.Logins)); + var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(MembersIdentityUser.Logins)); - if (UpdateMemberProperties(found, member)) + if (UpdateMemberProperties(found, user)) { _memberService.Save(found); } + // TODO: when to implement external login service? + //if (isLoginsPropertyDirty) //{ // _externalLoginService.Save( // found.Id, - // member.Logins.Select(x => new ExternalLogin( + // user.Logins.Select(x => new ExternalLogin( // x.LoginProvider, // x.ProviderKey, // x.UserData))); @@ -340,24 +154,8 @@ namespace Umbraco.Infrastructure.Security return Task.FromResult(IdentityResult.Success); } - private void ThrowIfDisposed() - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().Name); - } - } - - /// TODO: All from BackOfficeUserStore - same. Can we share? - /// - /// Set the user password hash - /// - /// The identity member user - /// The password hash - /// The cancellation token - /// Throws if the properties are null - /// Returns asynchronously - public Task SetPasswordHashAsync(MembersIdentityUser user, string passwordHash, CancellationToken cancellationToken = default(CancellationToken)) + /// + public override Task DeleteAsync(MembersIdentityUser user, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -366,50 +164,271 @@ namespace Umbraco.Infrastructure.Security throw new ArgumentNullException(nameof(user)); } - if (passwordHash == null) + IMember found = _memberService.GetById(UserIdToInt(user.Id)); + if (found != null) { - throw new ArgumentNullException(nameof(passwordHash)); + _memberService.Delete(found); } - if (string.IsNullOrEmpty(passwordHash)) + // TODO: when to implement external login service? + //_externalLoginService.DeleteUserLogins(UserIdToInt(user.Id)); + + return Task.FromResult(IdentityResult.Success); + } + + /// + public override Task FindByIdAsync(string userId, CancellationToken cancellationToken = default) => FindUserAsync(userId, cancellationToken); + + /// + protected override Task FindUserAsync(string userId, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + IMember user = _memberService.GetById(UserIdToInt(userId)); + if (user == null) { - throw new ArgumentException("Value can't be empty.", nameof(passwordHash)); + return Task.FromResult((MembersIdentityUser)null); } - user.PasswordHash = passwordHash; + return Task.FromResult(AssignLoginsCallback(_mapper.Map(user))); + } - // Clear this so that it's reset at the repository level - user.PasswordConfig = null; + /// + public override Task FindByNameAsync(string userName, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + IMember user = _memberService.GetByUsername(userName); + if (user == null) + { + return Task.FromResult((MembersIdentityUser)null); + } + + MembersIdentityUser result = AssignLoginsCallback(_mapper.Map(user)); + + return Task.FromResult(result); + } + + /// + public override async Task SetPasswordHashAsync(MembersIdentityUser user, string passwordHash, CancellationToken cancellationToken = default) + { + await base.SetPasswordHashAsync(user, passwordHash, cancellationToken); + + user.PasswordConfig = null; // Clear this so that it's reset at the repository level + user.LastPasswordChangeDateUtc = DateTime.UtcNow; + } + + /// + public override async Task HasPasswordAsync(MembersIdentityUser user, CancellationToken cancellationToken = default) + { + // This checks if it's null + var result = await base.HasPasswordAsync(user, cancellationToken); + if (result) + { + // we also want to check empty + return string.IsNullOrEmpty(user.PasswordHash) == false; + } + + return result; + } + + /// + public override Task FindByEmailAsync(string email, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + IMember member = _memberService.GetByEmail(email); + MembersIdentityUser result = member == null + ? null + : _mapper.Map(member); + + return Task.FromResult(AssignLoginsCallback(result)); + } + + /// + public override Task GetNormalizedEmailAsync(MembersIdentityUser user, CancellationToken cancellationToken) + => GetEmailAsync(user, cancellationToken); + + /// + public override Task SetNormalizedEmailAsync(MembersIdentityUser user, string normalizedEmail, CancellationToken cancellationToken) + => SetEmailAsync(user, normalizedEmail, cancellationToken); + + /// + public override Task AddLoginAsync(MembersIdentityUser user, UserLoginInfo login, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (login == null) + { + throw new ArgumentNullException(nameof(login)); + } + + ICollection logins = user.Logins; + var instance = new IdentityUserLogin(login.LoginProvider, login.ProviderKey, user.Id.ToString()); + IdentityUserLogin userLogin = instance; + logins.Add(userLogin); + + return Task.CompletedTask; + } + + /// + public override Task RemoveLoginAsync(MembersIdentityUser user, string loginProvider, string providerKey, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + IIdentityUserLogin userLogin = user.Logins.SingleOrDefault(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey); + if (userLogin != null) + { + user.Logins.Remove(userLogin); + } + + return Task.CompletedTask; + } + + /// + public override Task> GetLoginsAsync(MembersIdentityUser user, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + return Task.FromResult((IList)user.Logins.Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey, l.LoginProvider)).ToList()); + } + + + /// + protected override async Task> FindUserLoginAsync(string userId, string loginProvider, string providerKey, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + MembersIdentityUser user = await FindUserAsync(userId, cancellationToken); + if (user == null) + { + return null; + } + + IList logins = await GetLoginsAsync(user, cancellationToken); + UserLoginInfo found = logins.FirstOrDefault(x => x.ProviderKey == providerKey && x.LoginProvider == loginProvider); + if (found == null) + { + return null; + } + + return new IdentityUserLogin + { + LoginProvider = found.LoginProvider, + ProviderKey = found.ProviderKey, + ProviderDisplayName = found.ProviderDisplayName, // TODO: We don't store this value so it will be null + UserId = user.Id + }; + } + + /// + protected override Task> FindUserLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + // TODO: external login needed? + var logins = new List(); //_externalLoginService.Find(loginProvider, providerKey).ToList(); + if (logins.Count == 0) + { + return Task.FromResult((IdentityUserLogin)null); + } + + IIdentityUserLogin found = logins[0]; + return Task.FromResult(new IdentityUserLogin + { + LoginProvider = found.LoginProvider, + ProviderKey = found.ProviderKey, + ProviderDisplayName = null, // TODO: We don't store this value so it will be null + UserId = found.UserId + }); + } + + /// + /// Adds a user to a role (user group) + /// + public override Task AddToRoleAsync(MembersIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (normalizedRoleName == null) + { + throw new ArgumentNullException(nameof(normalizedRoleName)); + } + + if (string.IsNullOrWhiteSpace(normalizedRoleName)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(normalizedRoleName)); + } + + IdentityUserRole userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName); + + if (userRole == null) + { + user.AddRole(normalizedRoleName); + } return Task.CompletedTask; } /// - /// Get the user password hash + /// Removes the role (user group) for the user /// - /// - /// - /// - /// - public Task GetPasswordHashAsync(MembersIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + public override Task RemoveFromRoleAsync(MembersIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); + if (user == null) { throw new ArgumentNullException(nameof(user)); } - return Task.FromResult(user.PasswordHash); + if (normalizedRoleName == null) + { + throw new ArgumentNullException(nameof(normalizedRoleName)); + } + + if (string.IsNullOrWhiteSpace(normalizedRoleName)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(normalizedRoleName)); + } + + IdentityUserRole userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName); + + if (userRole != null) + { + user.Roles.Remove(userRole); + } + + return Task.CompletedTask; } /// - /// Returns true if a user has a password set + /// Returns the roles (user groups) for this user /// - /// The identity user - /// The cancellation token - /// True if the user has a password - public Task HasPasswordAsync(MembersIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + public override Task> GetRolesAsync(MembersIdentityUser user, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -418,8 +437,268 @@ namespace Umbraco.Infrastructure.Security throw new ArgumentNullException(nameof(user)); } - return Task.FromResult(string.IsNullOrEmpty(user.PasswordHash) == false); + return Task.FromResult((IList)user.Roles.Select(x => x.RoleId).ToList()); } + + /// + /// Returns true if a user is in the role + /// + public override Task IsInRoleAsync(MembersIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + return Task.FromResult(user.Roles.Select(x => x.RoleId).InvariantContains(normalizedRoleName)); + } + + /// + /// Lists all users of a given role. + /// + public override Task> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (normalizedRoleName == null) + { + throw new ArgumentNullException(nameof(normalizedRoleName)); + } + + IEnumerable members = _memberService.GetMembersByMemberType(normalizedRoleName); + + IList membersIdentityUsers = members.Select(x => _mapper.Map(x)).ToList(); + + return Task.FromResult(membersIdentityUsers); + } + + /// + protected override Task> FindRoleAsync(string normalizedRoleName, CancellationToken cancellationToken) + { + IMemberGroup group = _memberService.GetAllRolesTyped().SingleOrDefault(x => x.Name == normalizedRoleName); + if (group == null) + { + return Task.FromResult((IdentityRole)null); + } + + return Task.FromResult(new IdentityRole(group.Name) + { + //TODO: what should the alias be? + Id = @group.Id.ToString() + }); + } + + /// + protected override async Task> FindUserRoleAsync(string userId, string roleId, CancellationToken cancellationToken) + { + MembersIdentityUser user = await FindUserAsync(userId, cancellationToken); + if (user == null) + { + return null; + } + + IdentityUserRole found = user.Roles.FirstOrDefault(x => x.RoleId.InvariantEquals(roleId)); + return found; + } + + /// + public override Task GetSecurityStampAsync(MembersIdentityUser user, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + // 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.GenerateHash() + : user.SecurityStamp); + } + + // TODO: share all possible between backoffice user + + private MembersIdentityUser AssignLoginsCallback(MembersIdentityUser user) + { + if (user != null) + { + //TODO: when to + //user.SetLoginsCallback(new Lazy>(() => _externalLoginService.GetAll(UserIdToInt(user.Id)))); + } + + return user; + } + + private bool UpdateMemberProperties(IMember member, MembersIdentityUser identityUserMember) + { + var anythingChanged = false; + + // don't assign anything if nothing has changed as this will trigger the track changes of the model + if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.LastLoginDateUtc)) + || (member.LastLoginDate != default && identityUserMember.LastLoginDateUtc.HasValue == false) + || (identityUserMember.LastLoginDateUtc.HasValue && member.LastLoginDate.ToUniversalTime() != identityUserMember.LastLoginDateUtc.Value)) + { + anythingChanged = true; + + // if the LastLoginDate is being set to MinValue, don't convert it ToLocalTime + DateTime dt = identityUserMember.LastLoginDateUtc == DateTime.MinValue ? DateTime.MinValue : identityUserMember.LastLoginDateUtc.Value.ToLocalTime(); + member.LastLoginDate = dt; + } + + if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.LastPasswordChangeDateUtc)) + || (member.LastPasswordChangeDate != default && identityUserMember.LastPasswordChangeDateUtc.HasValue == false) + || (identityUserMember.LastPasswordChangeDateUtc.HasValue && member.LastPasswordChangeDate.ToUniversalTime() != identityUserMember.LastPasswordChangeDateUtc.Value)) + { + anythingChanged = true; + member.LastPasswordChangeDate = identityUserMember.LastPasswordChangeDateUtc.Value.ToLocalTime(); + } + + //if (identityUser.IsPropertyDirty(nameof(MembersIdentityUser.EmailConfirmed)) + // || (user.EmailConfirmedDate.HasValue && user.EmailConfirmedDate.Value != default && identityUser.EmailConfirmed == false) + // || ((user.EmailConfirmedDate.HasValue == false || user.EmailConfirmedDate.Value == default) && identityUser.EmailConfirmed)) + //{ + // anythingChanged = true; + // user.EmailConfirmedDate = identityUser.EmailConfirmed ? (DateTime?)DateTime.Now : null; + //} + + if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.Name)) + && member.Name != identityUserMember.Name && identityUserMember.Name.IsNullOrWhiteSpace() == false) + { + anythingChanged = true; + member.Name = identityUserMember.Name; + } + + if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.Email)) + && member.Email != identityUserMember.Email && identityUserMember.Email.IsNullOrWhiteSpace() == false) + { + anythingChanged = true; + member.Email = identityUserMember.Email; + } + + if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.AccessFailedCount)) + && member.FailedPasswordAttempts != identityUserMember.AccessFailedCount) + { + anythingChanged = true; + member.FailedPasswordAttempts = identityUserMember.AccessFailedCount; + } + + if (member.IsLockedOut != identityUserMember.IsLockedOut) + { + anythingChanged = true; + member.IsLockedOut = identityUserMember.IsLockedOut; + + if (member.IsLockedOut) + { + // need to set the last lockout date + member.LastLockoutDate = DateTime.Now; + } + } + + if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.UserName)) + && member.Username != identityUserMember.UserName && identityUserMember.UserName.IsNullOrWhiteSpace() == false) + { + anythingChanged = true; + member.Username = identityUserMember.UserName; + } + + if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.PasswordHash)) + && member.RawPasswordValue != identityUserMember.PasswordHash && identityUserMember.PasswordHash.IsNullOrWhiteSpace() == false) + { + anythingChanged = true; + member.RawPasswordValue = identityUserMember.PasswordHash; + member.PasswordConfiguration = identityUserMember.PasswordConfig; + } + + //if (user.SecurityStamp != identityUser.SecurityStamp) + //{ + // anythingChanged = true; + // user.SecurityStamp = identityUser.SecurityStamp; + //} + + // TODO: Fix this for Groups too (as per backoffice comment) + if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.Roles)) || identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.Groups))) + { + } + + // reset all changes + identityUserMember.ResetDirtyProperties(false); + + return anythingChanged; + } + + private static int UserIdToInt(string userId) + { + Attempt attempt = userId.TryConvertTo(); + if (attempt.Success) + { + return attempt.Result; + } + + throw new InvalidOperationException("Unable to convert user ID to int", attempt.Exception); + } + private static string UserIdToString(int userId) => string.Intern(userId.ToString()); + + /// + /// Not supported in Umbraco + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override Task> GetClaimsAsync(MembersIdentityUser user, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + /// + /// Not supported in Umbraco + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override Task AddClaimsAsync(MembersIdentityUser user, IEnumerable claims, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + /// + /// Not supported in Umbraco + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override Task ReplaceClaimAsync(MembersIdentityUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + /// + /// Not supported in Umbraco + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override Task RemoveClaimsAsync(MembersIdentityUser user, IEnumerable claims, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + /// + /// Not supported in Umbraco + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + // TODO: We should support these + + /// + /// Not supported in Umbraco + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected override Task> FindTokenAsync(MembersIdentityUser user, string loginProvider, string name, CancellationToken cancellationToken) => throw new NotImplementedException(); + + /// + /// Not supported in Umbraco + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected override Task AddUserTokenAsync(IdentityUserToken token) => throw new NotImplementedException(); + + /// + /// Not supported in Umbraco + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected override Task RemoveUserTokenAsync(IdentityUserToken token) => throw new NotImplementedException(); + } } diff --git a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs index 24c15957e2..b3cf2d5ee3 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs @@ -938,6 +938,20 @@ namespace Umbraco.Core.Services.Implement } } + /// + /// Returns a strongly typed list of all member groups + /// + /// + + public IEnumerable GetAllRolesTyped() + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.MemberTree); + return _memberGroupRepository.GetMany().Select(x=>x).Distinct(); + } + } + public IEnumerable GetAllRoles(int memberId) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs index a22810ff22..0e747c5dc5 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -208,6 +208,19 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services Assert.AreEqual(3, found.Count()); } + + [Test] + public void Can_Get_All_Roles_Typed() + { + MemberService.AddRole("MyTestRole1"); + MemberService.AddRole("MyTestRole2"); + MemberService.AddRole("MyTestRole3"); + + var found = MemberService.GetAllRolesTyped(); + + Assert.AreEqual(3, found.Count()); + } + [Test] public void Can_Get_All_Roles_IDs() { diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/UmbracoMemberIdentityUserStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/UmbracoMemberIdentityUserStoreTests.cs index c1a39e7b57..a71ef6544f 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/UmbracoMemberIdentityUserStoreTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/UmbracoMemberIdentityUserStoreTests.cs @@ -26,7 +26,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Members return new MembersUserStore( _mockMemberService.Object, new UmbracoMapper(new MapDefinitionCollection(new List())), - new Mock().Object); + new Mock().Object, + new IdentityErrorDescriber()); } [Test] diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs index aa8fea1d78..de10009dfd 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs @@ -81,7 +81,12 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers IBackOfficeSecurity backOfficeSecurity) { // arrange - Member member = SetupMemberTestData(umbracoMembersUserManager, memberTypeService, memberMapDefinition, backOfficeSecurityAccessor, backOfficeSecurity, out UmbracoMapper mapper, out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew); + Member member = SetupMemberTestData(memberMapDefinition, out UmbracoMapper mapper, out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew); + Mock.Get(umbracoMembersUserManager) + .Setup(x => x.CreateAsync(It.IsAny())) + .ReturnsAsync(() => IdentityResult.Success); + Mock.Get(memberTypeService).Setup(x => x.GetDefault()).Returns("fakeAlias"); + Mock.Get(backOfficeSecurityAccessor).Setup(x => x.BackOfficeSecurity).Returns(backOfficeSecurity); Mock.Get(memberService).SetupSequence( x => x.GetByEmail(It.IsAny())) @@ -110,7 +115,14 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers IBackOfficeSecurity backOfficeSecurity) { // arrange - Member member = SetupMemberTestData(umbracoMembersUserManager, memberTypeService, memberMapDefinition, backOfficeSecurityAccessor, backOfficeSecurity, out UmbracoMapper mapper, out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.Save); + Member member = SetupMemberTestData(memberMapDefinition, out UmbracoMapper mapper, out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew); + Mock.Get(umbracoMembersUserManager) + .Setup(x => x.FindByIdAsync(It.IsAny())) + .ReturnsAsync(() => new MembersIdentityUser()); + Mock.Get(umbracoMembersUserManager) + .Setup(x => x.UpdateAsync(new MembersIdentityUser())); + Mock.Get(memberTypeService).Setup(x => x.GetDefault()).Returns("fakeAlias"); + Mock.Get(backOfficeSecurityAccessor).Setup(x => x.BackOfficeSecurity).Returns(backOfficeSecurity); Mock.Get(memberService).SetupSequence( x => x.GetByEmail(It.IsAny())) @@ -139,7 +151,12 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers IBackOfficeSecurity backOfficeSecurity) { // arrange - Member member = SetupMemberTestData(umbracoMembersUserManager, memberTypeService, memberMapDefinition, backOfficeSecurityAccessor, backOfficeSecurity, out UmbracoMapper mapper, out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew); + Member member = SetupMemberTestData(memberMapDefinition, out UmbracoMapper mapper, out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew); + Mock.Get(umbracoMembersUserManager) + .Setup(x => x.CreateAsync(It.IsAny())) + .ReturnsAsync(() => IdentityResult.Success); + Mock.Get(memberTypeService).Setup(x => x.GetDefault()).Returns("fakeAlias"); + Mock.Get(backOfficeSecurityAccessor).Setup(x => x.BackOfficeSecurity).Returns(backOfficeSecurity); Mock.Get(memberService).SetupSequence( x => x.GetByEmail(It.IsAny())) @@ -158,22 +175,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers /// /// Setup all standard member data for test /// - private Member SetupMemberTestData(IMembersUserManager umbracoMembersUserManager, - IMemberTypeService memberTypeService, + private Member SetupMemberTestData( MapDefinitionCollection memberMapDefinition, - IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - IBackOfficeSecurity backOfficeSecurity, out UmbracoMapper mapper, out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction contentAction) { - Mock.Get(umbracoMembersUserManager) - .Setup(x => x.CreateAsync(It.IsAny())) - .ReturnsAsync(() => IdentityResult.Success); - Mock.Get(memberTypeService).Setup(x => x.GetDefault()).Returns("fakeAlias"); - Mock.Get(backOfficeSecurityAccessor).Setup(x => x.BackOfficeSecurity).Returns(backOfficeSecurity); - var memberType = new MemberType(new DefaultShortStringHelper(new DefaultShortStringHelperConfig()), int.MinValue); IMemberType testContentType = memberType; diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index e8cbc1114b..a75d63cebe 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -242,11 +242,10 @@ namespace Umbraco.Web.BackOffice.Controllers throw new ArgumentNullException(nameof(contentItem)); } - // TODO: this causes an issue when trying to correct an invalid model - //if (ModelState.IsValid == false) - //{ - // throw new HttpResponseException(HttpStatusCode.BadRequest, ModelState); - //} + if (ModelState.IsValid == false) + { + throw new HttpResponseException(HttpStatusCode.BadRequest, ModelState); + } // If we've reached here it means: // * Our model has been bound @@ -380,6 +379,18 @@ namespace Umbraco.Web.BackOffice.Controllers contentItem.Email, memberType.Alias, contentItem.Name); + + //if (contentItem.Password != null && !contentItem.Password.NewPassword.IsNullOrWhiteSpace()) + //{ + // // TODO: should we show the password rules? + // Task isPasswordValid = _memberManager.ValidatePasswordAsync(identityMember, contentItem.Password.NewPassword); + // if (isPasswordValid.Result == false) + // { + // ModelState.AddPropertyError( + // new ValidationResult($"Invalid password", new[] { "value" }), + // $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password"); + // } + //} IdentityResult created = await _memberManager.CreateAsync(identityMember, contentItem.Password.NewPassword); @@ -413,7 +424,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// The member to save private async Task UpdateMemberDataAsync(MemberSave contentItem) { - MembersIdentityUser identityMember = await _memberManager.FindByIdAsync(((int)contentItem.Id).ToString()); + MembersIdentityUser identityMember = await _memberManager.FindByIdAsync(contentItem.Id.ToString()); if (identityMember == null) { } @@ -497,18 +508,6 @@ namespace Umbraco.Web.BackOffice.Controllers new ValidationResult("Email address is already in use", new[] { "value" }), $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email"); } - - if (contentItem.Password != null && !contentItem.Password.NewPassword.IsNullOrWhiteSpace()) - { - //TODO: should we validate the password here, in advance? or when saving the identity user - //Task> result = _memberManager.ValidatePasswordAsync(contentItem.Password.NewPassword); - //if (result.Result.Exists(x => x.Succeeded == false)) - //{ - // ModelState.AddPropertyError( - // new ValidationResult($"Invalid password: {MapErrors(result.Result)}", new[] { "value" }), - // $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password"); - //} - } } private string MapErrors(List result) diff --git a/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs index dd82d73d62..395b242b56 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs @@ -63,7 +63,7 @@ namespace Umbraco.Web.BackOffice.Trees /// public ActionResult GetTreeNode(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings) { - //TODO: this is currently throwing an exception + //TODO: this is currently throwing an exception when loading a member var node = GetSingleTreeNode(id, queryStrings); //add the tree alias to the node since it is standalone (has no root for which this normally belongs) diff --git a/src/Umbraco.Web.Common/Security/MembersUserManager.cs b/src/Umbraco.Web.Common/Security/MembersUserManager.cs index 1a8c3e509a..6e4633934c 100644 --- a/src/Umbraco.Web.Common/Security/MembersUserManager.cs +++ b/src/Umbraco.Web.Common/Security/MembersUserManager.cs @@ -40,50 +40,7 @@ namespace Umbraco.Web.Common.Security { _httpContextAccessor = httpContextAccessor; } - - /// - /// Gets or sets the default members user password checker - /// - public IMembersUserPasswordChecker MembersUserPasswordChecker { get; set; } - // TODO: as per backoffice: This isn't a good way to set this, it needs to be injected - - /// - /// - /// By default this uses the standard ASP.Net Identity approach which is: - /// * Get password store - /// * Call VerifyPasswordAsync with the password store + user + password - /// * Uses the PasswordHasher.VerifyHashedPassword to compare the stored password - /// - /// In some cases people want simple custom control over the username/password check, for simplicity - /// sake, developers would like the users to simply validate against an LDAP directory but the user - /// data remains stored inside of Umbraco. - /// See: http://issues.umbraco.org/issue/U4-7032 for the use cases. - /// - /// We've allowed this check to be overridden with a simple callback so that developers don't actually - /// have to implement/override this class. - /// - public override async Task CheckPasswordAsync(MembersIdentityUser user, string password) - { - if (MembersUserPasswordChecker != null) - { - MembersUserPasswordCheckerResult result = await MembersUserPasswordChecker.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 != MembersUserPasswordCheckerResult.FallbackToDefaultChecker) - { - return result == MembersUserPasswordCheckerResult.ValidCredentials; - } - } - - // use the default behavior - return await base.CheckPasswordAsync(user, password); - } - + /// /// Override to check the user approval value as well as the user lock out date, by default this only checks the user's locked out date /// From aa1fd298cd0952f247576792108e003757cebe5a Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Tue, 8 Dec 2020 17:27:22 +0000 Subject: [PATCH 24/72] Re-included password for comparison --- .../Controllers/MemberController.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index a75d63cebe..d980e51fdf 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -379,18 +379,18 @@ namespace Umbraco.Web.BackOffice.Controllers contentItem.Email, memberType.Alias, contentItem.Name); - - //if (contentItem.Password != null && !contentItem.Password.NewPassword.IsNullOrWhiteSpace()) - //{ - // // TODO: should we show the password rules? - // Task isPasswordValid = _memberManager.ValidatePasswordAsync(identityMember, contentItem.Password.NewPassword); - // if (isPasswordValid.Result == false) - // { - // ModelState.AddPropertyError( - // new ValidationResult($"Invalid password", new[] { "value" }), - // $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password"); - // } - //} + + if (contentItem.Password != null && !contentItem.Password.NewPassword.IsNullOrWhiteSpace()) + { + // TODO: should we show the password rules? + Task isPasswordValid = _memberManager.CheckPasswordAsync(identityMember, contentItem.Password.NewPassword); + if (isPasswordValid.Result == false) + { + ModelState.AddPropertyError( + new ValidationResult($"Invalid password", new[] { "value" }), + $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password"); + } + } IdentityResult created = await _memberManager.CreateAsync(identityMember, contentItem.Password.NewPassword); From ec09c9b09f3e44401588a8455fc8fe09ddf520d7 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Wed, 9 Dec 2020 09:52:58 +0000 Subject: [PATCH 25/72] removed unwanted members folder --- src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index 373d7df0e0..b9368da89b 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -102,8 +102,4 @@ - - - - From 3f0e2f90548f83a20c346a3f11310cab218f23d8 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Wed, 9 Dec 2020 09:53:34 +0000 Subject: [PATCH 26/72] typo in tests --- .../MembersServiceCollectionExtensionsTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/MembersServiceCollectionExtensionsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/MembersServiceCollectionExtensionsTests.cs index 254bb8537f..64d62036ec 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/MembersServiceCollectionExtensionsTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/MembersServiceCollectionExtensionsTests.cs @@ -12,7 +12,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Web.BackOffice public class MembersServiceCollectionExtensionsTests : UmbracoIntegrationTest { [Test] - public void AddXMembersIdentity_ExpectMembersUserStoreResolvable() + public void AddMembersIdentity_ExpectMembersUserStoreResolvable() { IUserStore userStore = Services.GetService>(); From 6fb8724886ae5698f0c9c9ffe224f5e296993114 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Wed, 9 Dec 2020 10:44:30 +0000 Subject: [PATCH 27/72] Renamed tests --- ...yUserManagerTests.cs => MemberIdentityUserManagerTests.cs} | 2 +- ...ntityUserStoreTests.cs => MemberIdentityUserStoreTests.cs} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/{UmbracoMemberIdentityUserManagerTests.cs => MemberIdentityUserManagerTests.cs} (99%) rename src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/{UmbracoMemberIdentityUserStoreTests.cs => MemberIdentityUserStoreTests.cs} (96%) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/UmbracoMemberIdentityUserManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserManagerTests.cs similarity index 99% rename from src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/UmbracoMemberIdentityUserManagerTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserManagerTests.cs index c5aea4d1a3..b06b9d1a93 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/UmbracoMemberIdentityUserManagerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserManagerTests.cs @@ -18,7 +18,7 @@ using Umbraco.Web.Common.Security; namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Security { [TestFixture] - public class UmbracoMemberIdentityUserManagerTests + public class MemberIdentityUserManagerTests { private Mock> _mockMemberStore; private Mock> _mockIdentityOptions; diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/UmbracoMemberIdentityUserStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs similarity index 96% rename from src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/UmbracoMemberIdentityUserStoreTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs index a71ef6544f..e32cec4e89 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/UmbracoMemberIdentityUserStoreTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs @@ -13,10 +13,10 @@ using Umbraco.Core.Services; using Umbraco.Infrastructure.Security; using Umbraco.Tests.UnitTests.Umbraco.Core.ShortStringHelper; -namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Members +namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Security { [TestFixture] - public class UmbracoMemberIdentityUserStoreTests + public class MemberIdentityUserStoreTests { private Mock _mockMemberService; From 2c006ea99f63de0b08d182aa891c6b9888251ceb Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Wed, 9 Dec 2020 18:36:39 +0000 Subject: [PATCH 28/72] PR comments updated. Reorganised logic. Removed unused functionality. Started to reorganise update and save roles functionality. --- .../Models/Mapping/MemberMapDefinition.cs | 5 +- .../MembersUserPasswordCheckerResult.cs | 12 -- .../Security/IUmbracoUserManager.cs | 17 ++ .../Security/MembersIdentityUser.cs | 2 - .../Security/MembersUserStore.cs | 19 +- .../Security/UmbracoUserManager.cs | 49 +++++ .../Testing/UmbracoIntegrationTest.cs | 2 + ...MembersServiceCollectionExtensionsTests.cs | 1 + .../Controllers/MemberController.cs | 184 ++++++++++-------- ....cs => MemberIdentityBuilderExtensions.cs} | 11 +- .../MembersUserServiceCollectionExtensions.cs | 2 +- .../Extensions/UmbracoBuilderExtensions.cs | 1 + .../Mapping/MemberMapDefinition.cs | 7 +- .../Security/MembersUserManager.cs | 113 +---------- 14 files changed, 192 insertions(+), 233 deletions(-) delete mode 100644 src/Umbraco.Core/Security/MembersUserPasswordCheckerResult.cs rename src/Umbraco.Web.BackOffice/Extensions/{UmbracoMemberIdentityBuilderExtensions.cs => MemberIdentityBuilderExtensions.cs} (69%) diff --git a/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs index 60fe4daace..c17ef25c43 100644 --- a/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs @@ -9,7 +9,8 @@ namespace Umbraco.Core.Models.Mapping /// public void DefineMaps(UmbracoMapper mapper) => mapper.Define(Map); - // mappers + //TODO: put this here instead of a new mapper definition (like user). Can move + private static void Map(MemberSave source, IMember target, MapperContext context) { // TODO: ensure all properties are mapped as required @@ -18,8 +19,8 @@ namespace Umbraco.Core.Models.Mapping target.Email = source.Email; target.Key = source.Key; target.Username = source.Username; - target.Id = (int)(long)source.Id; target.Comments = source.Comments; + //TODO: add groups as required } } } diff --git a/src/Umbraco.Core/Security/MembersUserPasswordCheckerResult.cs b/src/Umbraco.Core/Security/MembersUserPasswordCheckerResult.cs deleted file mode 100644 index 3212609ed9..0000000000 --- a/src/Umbraco.Core/Security/MembersUserPasswordCheckerResult.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Umbraco.Core.Security -{ - /// - /// The result returned from the IMembersUserPasswordChecker - /// - public enum MembersUserPasswordCheckerResult - { - ValidCredentials, - InvalidCredentials, - FallbackToDefaultChecker - } -} diff --git a/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs b/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs index 30b11cc8a8..be4ffdf2e2 100644 --- a/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs @@ -243,6 +243,23 @@ namespace Umbraco.Infrastructure.Security /// A generated password string GeneratePassword(); + /// + /// Generates a hashed password for a null user based on the default password hasher + /// + /// The password to hash + /// The hashed password + string GeneratePassword(string password); + + /// + /// Used to validate the password without an identity user + /// Validation code is based on the default ValidatePasswordAsync code + /// Should return if validation is successful + /// + /// The password. + /// A representing whether validation was successful. + + Task ValidatePasswordAsync(string password); + /// /// Generates an email confirmation token for the specified user. /// diff --git a/src/Umbraco.Infrastructure/Security/MembersIdentityUser.cs b/src/Umbraco.Infrastructure/Security/MembersIdentityUser.cs index 4e13e6839d..e7b7450704 100644 --- a/src/Umbraco.Infrastructure/Security/MembersIdentityUser.cs +++ b/src/Umbraco.Infrastructure/Security/MembersIdentityUser.cs @@ -40,8 +40,6 @@ namespace Umbraco.Infrastructure.Security user.UserName = username; user.Email = email; user.MemberTypeAlias = memberTypeAlias; - // TODO: confirm if should be approved - user.IsApproved = true; user.Id = null; user.HasIdentity = false; user._name = name; diff --git a/src/Umbraco.Infrastructure/Security/MembersUserStore.cs b/src/Umbraco.Infrastructure/Security/MembersUserStore.cs index 70ca06a17a..aa28ee5d25 100644 --- a/src/Umbraco.Infrastructure/Security/MembersUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MembersUserStore.cs @@ -65,21 +65,18 @@ namespace Umbraco.Infrastructure.Security } // create member - // TODO: are we keeping this method, e.g. the Member Service? - // The user service creates it directly, but this way we get the member type by alias first + // TODO: are we keeping this method? The user service creates the member directly + // but this way we get the member type by alias first IMember memberEntity = _memberService.CreateMember( user.UserName, user.Email, user.Name.IsNullOrWhiteSpace() ? user.UserName : user.Name, user.MemberTypeAlias.IsNullOrWhiteSpace() ? Constants.Security.DefaultMemberTypeAlias : user.MemberTypeAlias); - // [from backofficeuser] we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. - var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(MembersIdentityUser.Logins)); UpdateMemberProperties(memberEntity, user); - // TODO: do we want to accept empty passwords here - if third-party for example? - // In other method if so? + // create the member _memberService.Save(memberEntity); if (!memberEntity.HasIdentity) @@ -90,7 +87,9 @@ namespace Umbraco.Infrastructure.Security // re-assign id user.Id = UserIdToString(memberEntity.Id); - //TODO: confirm re externallogins implementation + // [from backofficeuser] we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. + // var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(MembersIdentityUser.Logins)); + // TODO: confirm re externallogins implementation //if (isLoginsPropertyDirty) //{ // _externalLoginService.Save( @@ -101,9 +100,9 @@ namespace Umbraco.Infrastructure.Security // x.UserData))); //} - return Task.FromResult(IdentityResult.Success); - // TODO: confirm re roles implementations + + return Task.FromResult(IdentityResult.Success); } /// @@ -230,7 +229,7 @@ namespace Umbraco.Infrastructure.Security return string.IsNullOrEmpty(user.PasswordHash) == false; } - return result; + return false; } /// diff --git a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs index 6318218669..2000e108b7 100644 --- a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; @@ -111,6 +112,54 @@ namespace Umbraco.Core.Security return password; } + /// + /// Generates a hashed password based on the default password hasher + /// No existing identity user is required and this does not validate the password + /// + /// The password to hash + /// The hashed password + public string GeneratePassword(string password) + { + IPasswordHasher passwordHasher = GetDefaultPasswordHasher(PasswordConfiguration); + + string hashedPassword = passwordHasher.HashPassword(null, password); + return hashedPassword; + } + + /// + /// Used to validate the password without an identity user + /// Validation code is based on the default ValidatePasswordAsync code + /// Should return if validation is successful + /// + /// The password. + /// A representing whether validation was successful. + public async Task ValidatePasswordAsync(string password) + { + var errors = new List(); + var isValid = true; + foreach (IPasswordValidator v in PasswordValidators) + { + IdentityResult result = await v.ValidateAsync(this, null, password); + if (!result.Succeeded) + { + if (result.Errors.Any()) + { + errors.AddRange(result.Errors); + } + + isValid = false; + } + } + + if (!isValid) + { + Logger.LogWarning(14, "Password validation failed: {errors}.", string.Join(";", errors.Select(e => e.Code))); + return IdentityResult.Failed(errors.ToArray()); + } + + return IdentityResult.Success; + } + /// public override async Task CheckPasswordAsync(TUser user, string password) { diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index a8875de286..60bab5805f 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -196,6 +196,8 @@ namespace Umbraco.Tests.Integration.Testing builder.AddRuntimeMinifier(); builder.AddBackOffice(); builder.AddBackOfficeIdentity(); + builder.AddMembersIdentity(); + services.AddMvc(); diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/MembersServiceCollectionExtensionsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/MembersServiceCollectionExtensionsTests.cs index 64d62036ec..4991df7bd1 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/MembersServiceCollectionExtensionsTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/MembersServiceCollectionExtensionsTests.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using Umbraco.Extensions; using Umbraco.Infrastructure.Security; using Umbraco.Tests.Integration.Testing; +using Umbraco.Web.BackOffice.Extensions; namespace Umbraco.Tests.Integration.Umbraco.Web.BackOffice { diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index d980e51fdf..c13c02293d 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -237,16 +237,6 @@ namespace Umbraco.Web.BackOffice.Controllers [MemberSaveValidation] public async Task> PostSave([ModelBinder(typeof(MemberBinder))] MemberSave contentItem) { - if (contentItem == null) - { - throw new ArgumentNullException(nameof(contentItem)); - } - - if (ModelState.IsValid == false) - { - throw new HttpResponseException(HttpStatusCode.BadRequest, ModelState); - } - // If we've reached here it means: // * Our model has been bound // * and validated @@ -257,7 +247,7 @@ namespace Umbraco.Web.BackOffice.Controllers // map the properties to the persisted entity MapPropertyValues(contentItem); - ValidateMemberData(contentItem); + await ValidateMemberDataAsync(contentItem); // Unlike content/media - if there are errors for a member, we do NOT proceed to save them, we cannot so return the errors if (ModelState.IsValid == false) @@ -266,23 +256,14 @@ namespace Umbraco.Web.BackOffice.Controllers forDisplay.Errors = ModelState.ToErrorDictionary(); throw HttpResponseException.CreateValidationErrorResponse(forDisplay); } - - // We're gonna look up the current roles now because the below code can cause - // events to be raised and developers could be manually adding roles to members in - // their handlers. If we don't look this up now there's a chance we'll just end up - // removing the roles they've assigned. - IEnumerable currentRoles = _memberService.GetAllRoles(contentItem.PersistedContent.Username); - - // find the ones to remove and remove them - IEnumerable roles = currentRoles.ToList(); - string[] rolesToRemove = roles.Except(contentItem.Groups).ToArray(); - + // Depending on the action we need to first do a create or update using the membership manager // this ensures that passwords are formatted correctly and also performs the validation on the provider itself. + switch (contentItem.Action) { case ContentSaveAction.Save: - await UpdateMemberDataAsync(contentItem); + await UpdateMemberAsync(contentItem); break; case ContentSaveAction.SaveNew: await CreateMemberAsync(contentItem); @@ -295,22 +276,7 @@ namespace Umbraco.Web.BackOffice.Controllers // TODO: There's 3 things saved here and we should do this all in one transaction, // which we can do here by wrapping in a scope // but it would be nicer to have this taken care of within the Save method itself - - // Now let's do the role provider stuff - now that we've saved the content item (that is important since - // if we are changing the username, it must be persisted before looking up the member roles). - if (rolesToRemove.Any()) - { - _memberService.DissociateRoles(new[] { contentItem.PersistedContent.Username }, rolesToRemove); - } - - // find the ones to add and add them - string[] toAdd = contentItem.Groups.Except(roles).ToArray(); - if (toAdd.Any()) - { - // add the ones submitted - _memberService.AssignRoles(new[] { contentItem.PersistedContent.Username }, toAdd); - } - + // return the updated model MemberDisplay display = _umbracoMapper.Map(contentItem.PersistedContent); @@ -365,7 +331,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// Member content data /// The identity result of the created member - private async Task CreateMemberAsync(MemberSave contentItem) + private async Task CreateMemberAsync(MemberSave contentItem) { IMemberType memberType = _memberTypeService.Get(contentItem.ContentTypeAlias); if (memberType == null) @@ -373,25 +339,13 @@ namespace Umbraco.Web.BackOffice.Controllers throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}"); } - // Create the member with the MemberManager var identityMember = MembersIdentityUser.CreateNew( contentItem.Username, contentItem.Email, memberType.Alias, contentItem.Name); - if (contentItem.Password != null && !contentItem.Password.NewPassword.IsNullOrWhiteSpace()) - { - // TODO: should we show the password rules? - Task isPasswordValid = _memberManager.CheckPasswordAsync(identityMember, contentItem.Password.NewPassword); - if (isPasswordValid.Result == false) - { - ModelState.AddPropertyError( - new ValidationResult($"Invalid password", new[] { "value" }), - $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password"); - } - } - + // TODO: may not need to add password like this IdentityResult created = await _memberManager.CreateAsync(identityMember, contentItem.Password.NewPassword); if (created.Succeeded == false) @@ -402,19 +356,17 @@ namespace Umbraco.Web.BackOffice.Controllers // now re-look the member back up which will now exist IMember member = _memberService.GetByEmail(contentItem.Email); - member.CreatorId = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id; - // TODO: should this be removed since we've moved passwords out? - - member.RawPasswordValue = identityMember.PasswordHash; - member.Comments = contentItem.Comments; - - // since the back office user is creating this member, they will be set to approved - member.IsApproved = true; + var creatorId = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id; + member.CreatorId = creatorId; // map the save info over onto the user - member = _umbracoMapper.Map(contentItem, member); + member = _umbracoMapper.Map(contentItem, member); + + // now the member has been saved via identity, resave the member with mapped content properties + _memberService.Save(member); contentItem.PersistedContent = member; - return created; + + AddOrUpdateRoles(contentItem); } /// @@ -422,20 +374,8 @@ namespace Umbraco.Web.BackOffice.Controllers /// If the password has been reset then this method will return the reset/generated password, otherwise will return null. /// /// The member to save - private async Task UpdateMemberDataAsync(MemberSave contentItem) + private async Task UpdateMemberAsync(MemberSave contentItem) { - MembersIdentityUser identityMember = await _memberManager.FindByIdAsync(contentItem.Id.ToString()); - if (identityMember == null) - { - } - - IdentityResult updatedResult = await _memberManager.UpdateAsync(identityMember); - - if (updatedResult.Succeeded == false) - { - throw HttpResponseException.CreateNotificationValidationErrorResponse(updatedResult.Errors.ToErrorMessage()); - } - contentItem.PersistedContent.WriterId = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id; // If the user doesn't have access to sensitive values, then we need to check if any of the built in member property types @@ -476,21 +416,59 @@ namespace Umbraco.Web.BackOffice.Controllers ModelState.AddModelError("custom", "An admin cannot lock a user"); } - // no password changes then exit ? + MembersIdentityUser identityMember = await _memberManager.FindByIdAsync(contentItem.Id.ToString()); + if (identityMember == null) + { + throw HttpResponseException.CreateNotificationValidationErrorResponse("Member was not found"); + } + if (contentItem.Password != null) { - // TODO: set the password using Identity core - contentItem.PersistedContent.RawPasswordValue = _memberManager.GeneratePassword(); + IdentityResult validatePassword = await _memberManager.ValidatePasswordAsync(contentItem.Password.NewPassword); + if (validatePassword.Succeeded == false) + { + throw HttpResponseException.CreateNotificationValidationErrorResponse(validatePassword.Errors.ToErrorMessage()); + } + + string newPassword = _memberManager.GeneratePassword(contentItem.Password.NewPassword); + identityMember.PasswordHash = newPassword; } + + IdentityResult updatedResult = await _memberManager.UpdateAsync(identityMember); + + if (updatedResult.Succeeded == false) + { + throw HttpResponseException.CreateNotificationValidationErrorResponse(updatedResult.Errors.ToErrorMessage()); + } + + contentItem.PersistedContent.RawPasswordValue = identityMember.PasswordHash; + + _memberService.Save(contentItem.PersistedContent); + + AddOrUpdateRoles(contentItem); } - private void ValidateMemberData(MemberSave contentItem) + private async Task ValidateMemberDataAsync(MemberSave contentItem) { if (contentItem.Name.IsNullOrWhiteSpace()) { ModelState.AddPropertyError( new ValidationResult("Invalid user name", new[] { "value" }), $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login"); + return false; + } + + if (contentItem.Password != null && !contentItem.Password.NewPassword.IsNullOrWhiteSpace()) + { + // TODO: this currently stops the user interacting with the client-side when invalid + IdentityResult validPassword = await _memberManager.ValidatePasswordAsync(contentItem.Password.NewPassword); + if (!validPassword.Succeeded) + { + ModelState.AddPropertyError( + new ValidationResult("Invalid password: " + MapErrors(validPassword.Errors), new[] { "value" }), + $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password"); + return false; + } } IMember byUsername = _memberService.GetByUsername(contentItem.Username); @@ -499,6 +477,7 @@ namespace Umbraco.Web.BackOffice.Controllers ModelState.AddPropertyError( new ValidationResult("Username is already in use", new[] { "value" }), $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login"); + return false; } IMember byEmail = _memberService.GetByEmail(contentItem.Email); @@ -507,22 +486,57 @@ namespace Umbraco.Web.BackOffice.Controllers ModelState.AddPropertyError( new ValidationResult("Email address is already in use", new[] { "value" }), $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email"); + return false; } + + return true; } - private string MapErrors(List result) + private string MapErrors(IEnumerable result) { var sb = new StringBuilder(); - IEnumerable errors = result.Where(x => x.Succeeded == false); - - foreach (IdentityResult error in errors) + IEnumerable identityErrors = result.ToList(); + foreach (IdentityError error in identityErrors) { - sb.AppendLine(error.Errors.ToErrorMessage()); + string errorString = $"{error.Description}"; + sb.AppendLine(errorString); } return sb.ToString(); } + /// + /// TODO: refactor using identity roles + /// + /// + private void AddOrUpdateRoles(MemberSave contentItem) + { + // We're gonna look up the current roles now because the below code can cause + // events to be raised and developers could be manually adding roles to members in + // their handlers. If we don't look this up now there's a chance we'll just end up + // removing the roles they've assigned. + IEnumerable currentRoles = _memberService.GetAllRoles(contentItem.PersistedContent.Username); + + // find the ones to remove and remove them + IEnumerable roles = currentRoles.ToList(); + string[] rolesToRemove = roles.Except(contentItem.Groups).ToArray(); + + // Now let's do the role provider stuff - now that we've saved the content item (that is important since + // if we are changing the username, it must be persisted before looking up the member roles). + if (rolesToRemove.Any()) + { + _memberService.DissociateRoles(new[] { contentItem.PersistedContent.Username }, rolesToRemove); + } + + // find the ones to add and add them + string[] toAdd = contentItem.Groups.Except(roles).ToArray(); + if (toAdd.Any()) + { + // add the ones submitted + _memberService.AssignRoles(new[] { contentItem.PersistedContent.Username }, toAdd); + } + } + /// /// Permanently deletes a member /// diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoMemberIdentityBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/MemberIdentityBuilderExtensions.cs similarity index 69% rename from src/Umbraco.Web.BackOffice/Extensions/UmbracoMemberIdentityBuilderExtensions.cs rename to src/Umbraco.Web.BackOffice/Extensions/MemberIdentityBuilderExtensions.cs index 888d963891..e3551aa363 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoMemberIdentityBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/MemberIdentityBuilderExtensions.cs @@ -2,20 +2,19 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Umbraco.Infrastructure.Security; -namespace Umbraco.Extensions +namespace Umbraco.Web.BackOffice.Extensions { - public static class UmbracoMemberIdentityBuilderExtensions + public static class MemberIdentityBuilderExtensions { - /// - /// Adds a for the . + /// Adds a for the . /// /// The type of the user manager to add. /// /// The current instance. - public static IdentityBuilder AddUserManager(this IdentityBuilder identityBuilder) where TUserManager : UserManager, TInterface + public static IdentityBuilder AddUserManager(this IdentityBuilder identityBuilder) + where TUserManager : UserManager, TInterface { - identityBuilder.AddUserManager(); identityBuilder.Services.AddScoped(typeof(TInterface), typeof(TUserManager)); return identityBuilder; } diff --git a/src/Umbraco.Web.BackOffice/Extensions/MembersUserServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/MembersUserServiceCollectionExtensions.cs index 62b4904838..64e583b4b9 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/MembersUserServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/MembersUserServiceCollectionExtensions.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Umbraco.Infrastructure.Security; using Umbraco.Web.Common.Security; -namespace Umbraco.Extensions +namespace Umbraco.Web.BackOffice.Extensions { public static class MembersUserServiceCollectionExtensions { diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs index 8d35239e2a..82b2e88d5f 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs @@ -2,6 +2,7 @@ using System; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; using Umbraco.Core.DependencyInjection; +using Umbraco.Web.BackOffice.Extensions; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.BackOffice.Security; diff --git a/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs index 2df45704d8..b331d17fdc 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Umbraco.Core; using Umbraco.Core.Mapping; using Umbraco.Core.Models; @@ -93,14 +93,11 @@ namespace Umbraco.Web.BackOffice.Mapping target.Path = $"-1,{source.Id}"; target.Udi = Udi.Create(Constants.UdiEntityType.MemberGroup, source.Key); } - + // Umbraco.Code.MapAll private static void Map(IMember source, ContentPropertyCollectionDto target, MapperContext context) { target.Properties = context.MapEnumerable(source.Properties); } - - - } } diff --git a/src/Umbraco.Web.Common/Security/MembersUserManager.cs b/src/Umbraco.Web.Common/Security/MembersUserManager.cs index 6e4633934c..86ea7b1972 100644 --- a/src/Umbraco.Web.Common/Security/MembersUserManager.cs +++ b/src/Umbraco.Web.Common/Security/MembersUserManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Security.Principal; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -40,102 +41,6 @@ namespace Umbraco.Web.Common.Security { _httpContextAccessor = httpContextAccessor; } - - /// - /// Override to check the user approval value as well as the user lock out date, by default this only checks the user's locked out date - /// - /// The user - /// True if the user is locked out, else false - /// - /// In the ASP.NET Identity world, there is only one value for being locked out, in Umbraco we have 2 so when checking this for Umbraco we need to check both values - /// - public override async Task IsLockedOutAsync(MembersIdentityUser user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - if (user.IsApproved == false) - { - return true; - } - - return await base.IsLockedOutAsync(user); - } - - public override async Task AccessFailedAsync(MembersIdentityUser user) - { - IdentityResult result = await base.AccessFailedAsync(user); - - // Slightly confusing: this will return a Success if we successfully update the AccessFailed count - if (result.Succeeded) - { - RaiseLoginFailedEvent(_httpContextAccessor.HttpContext?.User, user.Id); - } - - return result; - } - - public override async Task ChangePasswordWithResetAsync(string userId, string token, string newPassword) - { - IdentityResult result = await base.ChangePasswordWithResetAsync(userId, token, newPassword); - if (result.Succeeded) - { - RaisePasswordChangedEvent(_httpContextAccessor.HttpContext?.User, userId); - } - - return result; - } - - public override async Task ChangePasswordAsync(MembersIdentityUser user, string currentPassword, string newPassword) - { - IdentityResult result = await base.ChangePasswordAsync(user, currentPassword, newPassword); - if (result.Succeeded) - { - RaisePasswordChangedEvent(_httpContextAccessor.HttpContext?.User, user.Id); - } - - return result; - } - - /// - public override async Task SetLockoutEndDateAsync(MembersIdentityUser user, DateTimeOffset? lockoutEnd) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - IdentityResult result = await base.SetLockoutEndDateAsync(user, lockoutEnd); - - // The way we unlock is by setting the lockoutEnd date to the current datetime - if (result.Succeeded && lockoutEnd >= DateTimeOffset.UtcNow) - { - RaiseAccountLockedEvent(_httpContextAccessor.HttpContext?.User, user.Id); - } - else - { - RaiseAccountUnlockedEvent(_httpContextAccessor.HttpContext?.User, user.Id); - - // Resets the login attempt fails back to 0 when unlock is clicked - await ResetAccessFailedCountAsync(user); - } - - return result; - } - - /// - public override async Task ResetAccessFailedCountAsync(MembersIdentityUser user) - { - IdentityResult result = await base.ResetAccessFailedCountAsync(user); - - // raise the event now that it's reset - RaiseResetAccessFailedCountEvent(_httpContextAccessor.HttpContext?.User, user.Id); - - return result; - } - private string GetCurrentUserId(IPrincipal currentUser) { UmbracoBackOfficeIdentity umbIdentity = currentUser?.GetUmbracoIdentity(); @@ -150,14 +55,8 @@ namespace Umbraco.Web.Common.Security return new IdentityAuditEventArgs(auditEvent, ip, currentUserId, string.Empty, affectedUserId, affectedUsername); } - private IdentityAuditEventArgs CreateArgs(AuditEvent auditEvent, BackOfficeIdentityUser currentUser, string affectedUserId, string affectedUsername) - { - var currentUserId = currentUser.Id; - var ip = IpResolver.GetCurrentRequestIpAddress(); - return new IdentityAuditEventArgs(auditEvent, ip, currentUserId, string.Empty, affectedUserId, affectedUsername); - } - // TODO: Review where these are raised and see if they can be simplified and either done in the this usermanager or the signin manager, + // TODO: As per backoffice, review where these are raised and see if they can be simplified and either done in the this usermanager or the signin manager, // lastly we'll resort to the authentication controller but we should try to remove all instances of that occuring public void RaiseAccountLockedEvent(IPrincipal currentUser, string userId) => OnAccountLocked(CreateArgs(AuditEvent.AccountLocked, currentUser, userId, string.Empty)); @@ -181,10 +80,6 @@ namespace Umbraco.Web.Common.Security return args; } - public void RaisePasswordChangedEvent(IPrincipal currentUser, string userId) => OnPasswordChanged(CreateArgs(AuditEvent.LogoutSuccess, currentUser, userId, string.Empty)); - - public void RaiseResetAccessFailedCountEvent(IPrincipal currentUser, string userId) => OnResetAccessFailedCount(CreateArgs(AuditEvent.ResetAccessFailedCount, currentUser, userId, string.Empty)); - public UserInviteEventArgs RaiseSendingUserInvite(IPrincipal currentUser, UserInvite invite, IUser createdUser) { var currentUserId = GetCurrentUserId(currentUser); @@ -196,9 +91,7 @@ namespace Umbraco.Web.Common.Security public bool HasSendingUserInviteEventHandler => SendingUserInvite != null; - // TODO: These static events are problematic. Moving forward we don't want static events at all but we cannot - // have non-static events here because the user manager is a Scoped instance not a singleton - // so we'll have to deal with this a diff way i.e. refactoring how events are done entirely + // TODO: Comments re static events as per backofficeusermanager public static event EventHandler AccountLocked; public static event EventHandler AccountUnlocked; public static event EventHandler ForgotPasswordRequested; From 67efe928580db096d8ec8a17a7a8c2128398f700 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Fri, 11 Dec 2020 14:32:13 +0000 Subject: [PATCH 29/72] Fixed tests, took out default data --- .../Controllers/MemberControllerUnitTests.cs | 96 +++++++++++++++---- .../Controllers/MemberController.cs | 5 + 2 files changed, 80 insertions(+), 21 deletions(-) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs index de10009dfd..3105a83a86 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Net; using System.Threading.Tasks; using AutoFixture.NUnit3; using Microsoft.AspNetCore.Identity; @@ -41,30 +42,55 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers ArgumentNullException exception = Assert.ThrowsAsync(() => sut.PostSave(null)); // assert - //Assert.That(exception.Message, Is.EqualTo("Exception of type 'Umbraco.Web.Common.Exceptions.HttpResponse...")); - //Assert.That(exception.HResult, Is.EqualTo(42)); + Assert.That(exception.Message, Is.EqualTo("Value cannot be null. (Parameter 'The member content item was null')")); } [Test] [AutoMoqData] public void PostSaveMember_WhenModelStateIsNotValid_ExpectFailureResponse( - MemberController sut) + [Frozen] IMembersUserManager umbracoMembersUserManager, + IMemberTypeService memberTypeService, + IDataTypeService dataTypeService, + IMemberService memberService, + MapDefinitionCollection memberMapDefinition, + PropertyEditorCollection propertyEditorCollection, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IBackOfficeSecurity backOfficeSecurity) { // arrange + Member member = SetupMemberTestData(memberMapDefinition, out UmbracoMapper mapper, out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew); + //Mock.Get(umbracoMembersUserManager) + // .Setup(x => x.CreateAsync(It.IsAny(), It.IsAny())) + // .ReturnsAsync(() => IdentityResult.Success); + //Mock.Get(umbracoMembersUserManager) + // .Setup(x => x.ValidatePasswordAsync(It.IsAny())) + // .ReturnsAsync(() => IdentityResult.Success); + //Mock.Get(memberTypeService).Setup(x => x.GetDefault()).Returns("fakeAlias"); + //Mock.Get(backOfficeSecurityAccessor).Setup(x => x.BackOfficeSecurity).Returns(backOfficeSecurity); + ////Mock.Get(memberService).SetupSequence( + // x => x.GetByEmail(It.IsAny())) + // .Returns(() => null) + // .Returns(() => member); + + MemberController sut = CreateSut(mapper, memberService, memberTypeService, umbracoMembersUserManager, dataTypeService, propertyEditorCollection, backOfficeSecurityAccessor); sut.ModelState.AddModelError("key", "Invalid model state"); - var fakeMemberData = new MemberSave() - { - Password = new ChangingPasswordModel() - { - Id = 123, - NewPassword = "i2ruf38vrba8^&T^", - OldPassword = null - } - }; + + + Mock.Get(umbracoMembersUserManager) + .Setup(x => x.CreateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(() => IdentityResult.Success); + Mock.Get(umbracoMembersUserManager) + .Setup(x => x.ValidatePasswordAsync(It.IsAny())) + .ReturnsAsync(() => IdentityResult.Success); + + var value = new MemberDisplay(); + string reason = "Validation failed"; // act + HttpResponseException exception = Assert.ThrowsAsync(() => sut.PostSave(fakeMemberData)); + // assert - Assert.ThrowsAsync(() => sut.PostSave(fakeMemberData)); + AssertExpectedException(exception, value, reason); } @@ -83,11 +109,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers // arrange Member member = SetupMemberTestData(memberMapDefinition, out UmbracoMapper mapper, out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew); Mock.Get(umbracoMembersUserManager) - .Setup(x => x.CreateAsync(It.IsAny())) + .Setup(x => x.CreateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(() => IdentityResult.Success); + Mock.Get(umbracoMembersUserManager) + .Setup(x => x.ValidatePasswordAsync(It.IsAny())) .ReturnsAsync(() => IdentityResult.Success); Mock.Get(memberTypeService).Setup(x => x.GetDefault()).Returns("fakeAlias"); Mock.Get(backOfficeSecurityAccessor).Setup(x => x.BackOfficeSecurity).Returns(backOfficeSecurity); - Mock.Get(memberService).SetupSequence( x => x.GetByEmail(It.IsAny())) .Returns(() => null) @@ -115,12 +143,21 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers IBackOfficeSecurity backOfficeSecurity) { // arrange - Member member = SetupMemberTestData(memberMapDefinition, out UmbracoMapper mapper, out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew); + Member member = SetupMemberTestData(memberMapDefinition, out UmbracoMapper mapper, out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.Save); Mock.Get(umbracoMembersUserManager) .Setup(x => x.FindByIdAsync(It.IsAny())) .ReturnsAsync(() => new MembersIdentityUser()); Mock.Get(umbracoMembersUserManager) - .Setup(x => x.UpdateAsync(new MembersIdentityUser())); + .Setup(x => x.ValidatePasswordAsync(It.IsAny())) + .ReturnsAsync(() => IdentityResult.Success); + + string password = "fakepassword9aw89rnyco3938cyr^%&*()i8Y"; + Mock.Get(umbracoMembersUserManager) + .Setup(x => x.GeneratePassword(It.IsAny())) + .Returns(password); + Mock.Get(umbracoMembersUserManager) + .Setup(x => x.UpdateAsync(It.IsAny())) + .ReturnsAsync(() => IdentityResult.Success); Mock.Get(memberTypeService).Setup(x => x.GetDefault()).Returns("fakeAlias"); Mock.Get(backOfficeSecurityAccessor).Setup(x => x.BackOfficeSecurity).Returns(backOfficeSecurity); @@ -140,7 +177,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers [Test] [AutoMoqData] - public async Task PostSaveMember_SaveNew_WhenMemberEmailAlreadyExists_ExpectSuccessResponse( + public void PostSaveMember_SaveNew_WhenMemberEmailAlreadyExists_ExpectSuccessResponse( [Frozen] IMembersUserManager umbracoMembersUserManager, IMemberTypeService memberTypeService, IDataTypeService dataTypeService, @@ -163,13 +200,29 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers .Returns(() => member); MemberController sut = CreateSut(mapper, memberService, memberTypeService, umbracoMembersUserManager, dataTypeService, propertyEditorCollection, backOfficeSecurityAccessor); + var value = new MemberDisplay(); + string reason = "Validation failed"; // act HttpResponseException exception = Assert.ThrowsAsync(() => sut.PostSave(fakeMemberData)); // assert - //Assert.That(exception.Message, Is.EqualTo("Exception of type 'Umbraco.Web.Common.Exceptions.HttpResponse...")); - //Assert.That(exception.Value, Is.EqualTo(42)); + AssertExpectedException(exception, value, reason); + } + + private void AssertExpectedException(HttpResponseException exception, object value, string reason) + { + var expectedException = new HttpResponseException(HttpStatusCode.BadRequest, value) + { + AdditionalHeaders = + { + ["X-Status-Reason"] = reason + } + }; + + Assert.That(exception.AdditionalHeaders, Is.EqualTo(expectedException.AdditionalHeaders)); + Assert.That(exception.Value, Is.EqualTo(expectedException.Value)); + Assert.That(exception.Status, Is.EqualTo(expectedException.Status)); } /// @@ -190,7 +243,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers var testEmail = "test@umbraco.com"; var testUser = "TestUser"; - var member = new Member(testName, testEmail, testUser, testContentType) {RawPasswordValue = fakePassword}; + var member = new Member(testName, testEmail, testUser, testContentType) { RawPasswordValue = fakePassword }; mapper = new UmbracoMapper(memberMapDefinition); // TODO: reuse maps @@ -238,6 +291,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers { var fakeMemberData = new MemberSave() { + Id = 123, Password = new ChangingPasswordModel() { Id = 123, diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index c13c02293d..957863704b 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -237,6 +237,11 @@ namespace Umbraco.Web.BackOffice.Controllers [MemberSaveValidation] public async Task> PostSave([ModelBinder(typeof(MemberBinder))] MemberSave contentItem) { + if (contentItem == null) + { + throw new ArgumentNullException("The member content item was null"); + } + // If we've reached here it means: // * Our model has been bound // * and validated From c5c01146b59e7edd108581dd50f0c77f70c73999 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Fri, 11 Dec 2020 14:35:25 +0000 Subject: [PATCH 30/72] Remove commented out --- .../Controllers/MemberControllerUnitTests.cs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs index 3105a83a86..1beab6257c 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs @@ -59,19 +59,6 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers { // arrange Member member = SetupMemberTestData(memberMapDefinition, out UmbracoMapper mapper, out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew); - //Mock.Get(umbracoMembersUserManager) - // .Setup(x => x.CreateAsync(It.IsAny(), It.IsAny())) - // .ReturnsAsync(() => IdentityResult.Success); - //Mock.Get(umbracoMembersUserManager) - // .Setup(x => x.ValidatePasswordAsync(It.IsAny())) - // .ReturnsAsync(() => IdentityResult.Success); - //Mock.Get(memberTypeService).Setup(x => x.GetDefault()).Returns("fakeAlias"); - //Mock.Get(backOfficeSecurityAccessor).Setup(x => x.BackOfficeSecurity).Returns(backOfficeSecurity); - ////Mock.Get(memberService).SetupSequence( - // x => x.GetByEmail(It.IsAny())) - // .Returns(() => null) - // .Returns(() => member); - MemberController sut = CreateSut(mapper, memberService, memberTypeService, umbracoMembersUserManager, dataTypeService, propertyEditorCollection, backOfficeSecurityAccessor); sut.ModelState.AddModelError("key", "Invalid model state"); From d285f626a0f7e0430deef87f75c989b5eb65265b Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Mon, 11 Jan 2021 14:24:49 +0000 Subject: [PATCH 31/72] Pull request peer review changes - renamed password method, removed unneeded non-strongly typed Member GetAllRoles() method, removed duplicate Normalizer test and rename normalizer, removed currently unneeded events on MembersUserManager. Fixed namespaces and ensured tests in correct location. --- .../Mapping/MemberTabsAndPropertiesMapper.cs | 2 +- .../Security/UmbracoBackOfficeIdentity.cs | 1 - .../Services/IMembershipRoleService.cs | 1 - .../UmbracoBuilder.MappingProfiles.cs | 1 + .../Install/InstallSteps/NewInstallStep.cs | 3 +- .../BackOfficeClaimsPrincipalFactory.cs | 2 +- .../Security/BackOfficeIdentityBuilder.cs | 2 +- .../BackOfficeIdentityErrorDescriber.cs | 2 +- .../Security/BackOfficeIdentityOptions.cs | 2 +- .../Security/BackOfficeIdentityUser.cs | 4 +- .../Security/BackOfficeUserStore.cs | 1 + .../Security/BackOfficeUserValidator.cs | 1 + .../Security/IBackOfficeUserManager.cs | 5 +- .../IBackOfficeUserPasswordChecker.cs | 3 +- .../Security/IMembersUserManager.cs | 5 -- .../Security/IUmbracoUserManager.cs | 4 +- .../Security/IUserSessionStore.cs | 2 +- .../Security/IdentityExtensions.cs | 4 +- .../Security/IdentityMapDefinition.cs | 5 +- .../Security/MembersIdentityBuilder.cs | 1 - .../Security/MembersUserStore.cs | 3 +- ...pNormalizer.cs => NoOpLookupNormalizer.cs} | 4 +- .../Security/SignOutAuditEventArgs.cs | 4 +- .../Security/UmbracoUserManager.cs | 13 ++- .../Security/UserInviteEventArgs.cs | 2 +- .../Services/Implement/MemberService.cs | 11 +-- .../TestServerTest/TestAuthHandler.cs | 3 +- .../Services/MemberServiceTests.cs | 8 +- ...kOfficeServiceCollectionExtensionsTests.cs | 1 + .../AutoFixture/AutoMoqDataAttribute.cs | 1 + .../BackOffice/IdentityExtensionsTests.cs | 2 +- .../BackOfficeClaimsPrincipalFactoryTests.cs | 3 +- .../BackOfficeLookupNormalizerTests.cs | 60 ------------- .../MemberIdentityUserManagerTests.cs | 1 - .../Security/NoOpLookupNormalizerTests.cs} | 46 +++++----- .../Controllers/MemberControllerUnitTests.cs | 2 +- .../Controllers/UsersControllerTests.cs | 1 + .../Controllers/AuthenticationController.cs | 1 + .../Controllers/BackOfficeController.cs | 1 + .../Controllers/CurrentUserController.cs | 1 + .../Controllers/MemberController.cs | 2 +- .../Controllers/UsersController.cs | 1 + .../ServiceCollectionExtensions.cs | 4 +- .../CheckIfUserTicketDataIsStaleAttribute.cs | 1 + .../Security/BackOfficePasswordHasher.cs | 3 +- .../BackOfficeSecurityStampValidator.cs | 3 +- .../Security/BackOfficeSessionIdValidator.cs | 1 + .../Security/BackOfficeSignInManager.cs | 3 +- .../Security/BackOfficeUserManagerAuditer.cs | 1 + .../ConfigureBackOfficeIdentityOptions.cs | 1 + .../Security/ExternalSignInAutoLinkOptions.cs | 3 +- .../Security/IBackOfficeSignInManager.cs | 3 +- .../Security/PasswordChanger.cs | 1 + .../Middleware/BootFailedMiddleware.cs | 2 +- .../Security/BackOfficeUserManager.cs | 4 +- .../Security/MembersUserManager.cs | 86 ++----------------- ...eDirectoryBackOfficeUserPasswordChecker.cs | 1 + .../IBackOfficeUserPasswordChecker.cs | 3 +- .../Security/Providers/MembersRoleProvider.cs | 25 +++--- 59 files changed, 120 insertions(+), 247 deletions(-) rename src/Umbraco.Infrastructure/Security/{BackOfficeLookupNormalizer.cs => NoOpLookupNormalizer.cs} (76%) rename src/Umbraco.Tests.UnitTests/{Umbraco.Core => Umbraco.Infrastructure}/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs (98%) delete mode 100644 src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackOffice/BackOfficeLookupNormalizerTests.cs rename src/Umbraco.Tests.UnitTests/{Umbraco.Core/BackOffice/NopLookupNormalizerTests.cs => Umbraco.Infrastructure/Security/NoOpLookupNormalizerTests.cs} (78%) diff --git a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs index 2f1e76b834..faf811607e 100644 --- a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Options; diff --git a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs index 5fd9f23c92..6c9002f095 100644 --- a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs +++ b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs @@ -5,7 +5,6 @@ using System.Security.Claims; namespace Umbraco.Core.Security { - /// /// A custom user identity for the Umbraco backoffice /// diff --git a/src/Umbraco.Core/Services/IMembershipRoleService.cs b/src/Umbraco.Core/Services/IMembershipRoleService.cs index 73479801d2..e0d1e21e7a 100644 --- a/src/Umbraco.Core/Services/IMembershipRoleService.cs +++ b/src/Umbraco.Core/Services/IMembershipRoleService.cs @@ -9,7 +9,6 @@ namespace Umbraco.Core.Services where T : class, IMembershipUser { void AddRole(string roleName); - IEnumerable GetAllRoles(); IEnumerable GetAllRolesTyped(); IEnumerable GetAllRoles(int memberId); IEnumerable GetAllRoles(string username); diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.MappingProfiles.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.MappingProfiles.cs index 692342581e..2973abf271 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.MappingProfiles.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.MappingProfiles.cs @@ -3,6 +3,7 @@ using Umbraco.Core.DependencyInjection; using Umbraco.Core.Mapping; using Umbraco.Core.Models.Mapping; using Umbraco.Core.Security; +using Umbraco.Infrastructure.Security; using Umbraco.Web.Models.Mapping; namespace Umbraco.Infrastructure.DependencyInjection diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs index b7699b7d0e..b746011494 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Specialized; using System.Net.Http; using System.Text; @@ -11,6 +11,7 @@ using Umbraco.Core.Migrations.Install; using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Extensions; +using Umbraco.Infrastructure.Security; using Umbraco.Web.Install.Models; namespace Umbraco.Web.Install.InstallSteps diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs b/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs index 77f707d812..0746e82076 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using Umbraco.Core.Security; -namespace Umbraco.Core.Security +namespace Umbraco.Infrastructure.Security { /// /// A diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityBuilder.cs b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityBuilder.cs index c9f8d35ada..bf0f61bf35 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityBuilder.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityBuilder.cs @@ -3,7 +3,7 @@ using System.Reflection; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; -namespace Umbraco.Core.Security +namespace Umbraco.Infrastructure.Security { public class BackOfficeIdentityBuilder : IdentityBuilder { diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityErrorDescriber.cs b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityErrorDescriber.cs index 6d36e489b8..38472c38d7 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityErrorDescriber.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityErrorDescriber.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Identity; -namespace Umbraco.Core.Security +namespace Umbraco.Infrastructure.Security { /// /// Umbraco back office specific diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityOptions.cs b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityOptions.cs index 77849c4d0c..f599769938 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityOptions.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityOptions.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Identity; -namespace Umbraco.Core.Security +namespace Umbraco.Infrastructure.Security { /// /// Identity options specifically for the back office identity implementation diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs index e2e8031768..ae9b351b65 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs @@ -2,12 +2,12 @@ using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Identity; +using Umbraco.Core; using Umbraco.Core.Configuration.Models; -using Umbraco.Core.Models.Entities; using Umbraco.Core.Models.Identity; using Umbraco.Core.Models.Membership; -namespace Umbraco.Core.Security +namespace Umbraco.Infrastructure.Security { /// /// The identity user used for the back office diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs index 1756e84d76..a7d925e580 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs @@ -15,6 +15,7 @@ using Umbraco.Core.Models.Identity; using Umbraco.Core.Models.Membership; using Umbraco.Core.Scoping; using Umbraco.Core.Services; +using Umbraco.Infrastructure.Security; namespace Umbraco.Core.Security { diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeUserValidator.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserValidator.cs index 8b2c8932a7..a1953159e9 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeUserValidator.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserValidator.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Umbraco.Core.Security; +using Umbraco.Infrastructure.Security; namespace Umbraco.Core.Security { diff --git a/src/Umbraco.Infrastructure/Security/IBackOfficeUserManager.cs b/src/Umbraco.Infrastructure/Security/IBackOfficeUserManager.cs index cc0a63142b..c65570405f 100644 --- a/src/Umbraco.Infrastructure/Security/IBackOfficeUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/IBackOfficeUserManager.cs @@ -1,7 +1,4 @@ -using Umbraco.Core.Security; -using Umbraco.Infrastructure.Security; - -namespace Umbraco.Core.Security +namespace Umbraco.Infrastructure.Security { /// /// The user manager for the back office diff --git a/src/Umbraco.Infrastructure/Security/IBackOfficeUserPasswordChecker.cs b/src/Umbraco.Infrastructure/Security/IBackOfficeUserPasswordChecker.cs index fdf1f1fcf2..f6f90ef82d 100644 --- a/src/Umbraco.Infrastructure/Security/IBackOfficeUserPasswordChecker.cs +++ b/src/Umbraco.Infrastructure/Security/IBackOfficeUserPasswordChecker.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; +using Umbraco.Core.Security; -namespace Umbraco.Core.Security +namespace Umbraco.Infrastructure.Security { /// /// Used by the BackOfficeUserManager to check the username/password which allows for developers to more easily diff --git a/src/Umbraco.Infrastructure/Security/IMembersUserManager.cs b/src/Umbraco.Infrastructure/Security/IMembersUserManager.cs index a5b0579cb7..a466ea2aa9 100644 --- a/src/Umbraco.Infrastructure/Security/IMembersUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/IMembersUserManager.cs @@ -1,8 +1,3 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -using Umbraco.Core.Security; - namespace Umbraco.Infrastructure.Security { /// diff --git a/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs b/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs index be4ffdf2e2..fa3d7a691a 100644 --- a/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs @@ -244,11 +244,11 @@ namespace Umbraco.Infrastructure.Security string GeneratePassword(); /// - /// Generates a hashed password for a null user based on the default password hasher + /// Hashes a password for a null user based on the default password hasher /// /// The password to hash /// The hashed password - string GeneratePassword(string password); + string HashPassword(string password); /// /// Used to validate the password without an identity user diff --git a/src/Umbraco.Infrastructure/Security/IUserSessionStore.cs b/src/Umbraco.Infrastructure/Security/IUserSessionStore.cs index c68d1f13f9..bfdf296156 100644 --- a/src/Umbraco.Infrastructure/Security/IUserSessionStore.cs +++ b/src/Umbraco.Infrastructure/Security/IUserSessionStore.cs @@ -1,6 +1,6 @@ using System.Threading.Tasks; -namespace Umbraco.Core.Security +namespace Umbraco.Infrastructure.Security { /// /// An IUserStore interface part to implement if the store supports validating user session Ids diff --git a/src/Umbraco.Infrastructure/Security/IdentityExtensions.cs b/src/Umbraco.Infrastructure/Security/IdentityExtensions.cs index 95a63c6001..911baf2c33 100644 --- a/src/Umbraco.Infrastructure/Security/IdentityExtensions.cs +++ b/src/Umbraco.Infrastructure/Security/IdentityExtensions.cs @@ -1,9 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Identity; -namespace Umbraco.Extensions +namespace Umbraco.Infrastructure.Security { public static class IdentityExtensions { diff --git a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs index 00239b21eb..88dd6eaeb7 100644 --- a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs +++ b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs @@ -1,14 +1,13 @@ using System; using Microsoft.Extensions.Options; -using Umbraco.Core.Configuration; +using Umbraco.Core; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; -using Umbraco.Infrastructure.Security; -namespace Umbraco.Core.Security +namespace Umbraco.Infrastructure.Security { public class IdentityMapDefinition : IMapDefinition { diff --git a/src/Umbraco.Infrastructure/Security/MembersIdentityBuilder.cs b/src/Umbraco.Infrastructure/Security/MembersIdentityBuilder.cs index 553cca6a17..dde340f563 100644 --- a/src/Umbraco.Infrastructure/Security/MembersIdentityBuilder.cs +++ b/src/Umbraco.Infrastructure/Security/MembersIdentityBuilder.cs @@ -7,7 +7,6 @@ namespace Umbraco.Infrastructure.Security { public class MembersIdentityBuilder : IdentityBuilder { - public MembersIdentityBuilder(IServiceCollection services) : base(typeof(MembersIdentityUser), services) { } diff --git a/src/Umbraco.Infrastructure/Security/MembersUserStore.cs b/src/Umbraco.Infrastructure/Security/MembersUserStore.cs index aa28ee5d25..c79e18c168 100644 --- a/src/Umbraco.Infrastructure/Security/MembersUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MembersUserStore.cs @@ -307,8 +307,7 @@ namespace Umbraco.Infrastructure.Security return Task.FromResult((IList)user.Logins.Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey, l.LoginProvider)).ToList()); } - - + /// protected override async Task> FindUserLoginAsync(string userId, string loginProvider, string providerKey, CancellationToken cancellationToken) { diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeLookupNormalizer.cs b/src/Umbraco.Infrastructure/Security/NoOpLookupNormalizer.cs similarity index 76% rename from src/Umbraco.Infrastructure/Security/BackOfficeLookupNormalizer.cs rename to src/Umbraco.Infrastructure/Security/NoOpLookupNormalizer.cs index 957e36d1d0..c81a46e726 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeLookupNormalizer.cs +++ b/src/Umbraco.Infrastructure/Security/NoOpLookupNormalizer.cs @@ -1,12 +1,12 @@ using Microsoft.AspNetCore.Identity; -namespace Umbraco.Core.Security +namespace Umbraco.Infrastructure.Security { /// /// No-op lookup normalizer to maintain compatibility with ASP.NET Identity 2 /// - public class BackOfficeLookupNormalizer : ILookupNormalizer + public class NoOpLookupNormalizer : ILookupNormalizer { // TODO: Do we need this? diff --git a/src/Umbraco.Infrastructure/Security/SignOutAuditEventArgs.cs b/src/Umbraco.Infrastructure/Security/SignOutAuditEventArgs.cs index 626932640c..dfdf3bff7d 100644 --- a/src/Umbraco.Infrastructure/Security/SignOutAuditEventArgs.cs +++ b/src/Umbraco.Infrastructure/Security/SignOutAuditEventArgs.cs @@ -1,8 +1,8 @@ +using Umbraco.Core; using Umbraco.Core.Security; -namespace Umbraco.Core.Security +namespace Umbraco.Infrastructure.Security { - /// /// Event args used when signing out /// diff --git a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs index 2000e108b7..cac0d1ea78 100644 --- a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs @@ -8,11 +8,11 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Core.Configuration; using Umbraco.Core.Models.Identity; +using Umbraco.Core.Security; using Umbraco.Net; -namespace Umbraco.Core.Security +namespace Umbraco.Infrastructure.Security { - /// /// Abstract class for Umbraco User Managers for back office users or front-end members /// @@ -34,12 +34,11 @@ namespace Umbraco.Core.Security IPasswordHasher passwordHasher, IEnumerable> userValidators, IEnumerable> passwordValidators, - ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger> logger, IOptions passwordConfiguration) - : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) + : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, new NoOpLookupNormalizer(), errors, services, logger) { IpResolver = ipResolver ?? throw new ArgumentNullException(nameof(ipResolver)); PasswordConfiguration = passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration)); @@ -74,8 +73,8 @@ namespace Umbraco.Core.Security /// Used to validate a user's session /// /// The user id - /// The sesion id - /// True if the sesion is valid, else false + /// The session id + /// True if the session is valid, else false public virtual async Task ValidateSessionIdAsync(string userId, string sessionId) { var userSessionStore = Store as IUserSessionStore; @@ -118,7 +117,7 @@ namespace Umbraco.Core.Security /// /// The password to hash /// The hashed password - public string GeneratePassword(string password) + public string HashPassword(string password) { IPasswordHasher passwordHasher = GetDefaultPasswordHasher(PasswordConfiguration); diff --git a/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs b/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs index 80b05497a8..ee2930a71b 100644 --- a/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs +++ b/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs @@ -2,7 +2,7 @@ using Umbraco.Core.Models.Membership; using Umbraco.Core.Security; using Umbraco.Web.Models.ContentEditing; -namespace Umbraco.Core.Security +namespace Umbraco.Infrastructure.Security { public class UserInviteEventArgs : IdentityAuditEventArgs { diff --git a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs index b3cf2d5ee3..52f2fe0836 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs @@ -928,16 +928,7 @@ namespace Umbraco.Core.Services.Implement scope.Complete(); } } - - public IEnumerable GetAllRoles() - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.MemberTree); - return _memberGroupRepository.GetMany().Select(x => x.Name).Distinct(); - } - } - + /// /// Returns a strongly typed list of all member groups /// diff --git a/src/Umbraco.Tests.Integration/TestServerTest/TestAuthHandler.cs b/src/Umbraco.Tests.Integration/TestServerTest/TestAuthHandler.cs index ab5821c81c..2f567418e6 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/TestAuthHandler.cs +++ b/src/Umbraco.Tests.Integration/TestServerTest/TestAuthHandler.cs @@ -1,4 +1,4 @@ -using System.Text.Encodings.Web; +using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Logging; @@ -8,6 +8,7 @@ using Umbraco.Core.Mapping; using Umbraco.Core.Models.Membership; using Umbraco.Core.Security; using Umbraco.Core.Services; +using Umbraco.Infrastructure.Security; using Umbraco.Web.Common.Security; namespace Umbraco.Tests.Integration.TestServerTest diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs index 0e747c5dc5..a286b0ec42 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs @@ -179,7 +179,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services { MemberService.AddRole("MyTestRole"); - var found = MemberService.GetAllRoles(); + IEnumerable found = MemberService.GetAllRolesTyped(); Assert.AreEqual(1, found.Count()); Assert.AreEqual("MyTestRole", found.Single()); @@ -191,7 +191,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services MemberService.AddRole("MyTestRole"); MemberService.AddRole("MyTestRole"); - var found = MemberService.GetAllRoles(); + IEnumerable found = MemberService.GetAllRolesTyped(); Assert.AreEqual(1, found.Count()); Assert.AreEqual("MyTestRole", found.Single()); @@ -204,7 +204,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services MemberService.AddRole("MyTestRole2"); MemberService.AddRole("MyTestRole3"); - var found = MemberService.GetAllRoles(); + IEnumerable found = MemberService.GetAllRolesTyped(); Assert.AreEqual(3, found.Count()); } @@ -298,7 +298,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services MemberService.DeleteRole("MyTestRole1", false); - var memberRoles = MemberService.GetAllRoles(); + IEnumerable memberRoles = MemberService.GetAllRolesTyped(); Assert.AreEqual(0, memberRoles.Count()); } diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs index 37863da472..65a90b9ab7 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using Umbraco.Core.DependencyInjection; using Umbraco.Core.Security; +using Umbraco.Infrastructure.Security; using Umbraco.Tests.Integration.Testing; using Umbraco.Web.BackOffice.DependencyInjection; diff --git a/src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs b/src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs index b0eac41478..8097cbed92 100644 --- a/src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs +++ b/src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs @@ -15,6 +15,7 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Hosting; using Umbraco.Core.Security; +using Umbraco.Infrastructure.Security; using Umbraco.Web.BackOffice.Controllers; using Umbraco.Web.BackOffice.Routing; using Umbraco.Web.Common.Install; diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/IdentityExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/IdentityExtensionsTests.cs index 3b2d0391e2..c758272bf4 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/IdentityExtensionsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/IdentityExtensionsTests.cs @@ -5,7 +5,7 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.Identity; using NUnit.Framework; -using Umbraco.Extensions; +using Umbraco.Infrastructure.Security; namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice { diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs similarity index 98% rename from src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs index e681fc6841..c8aa619226 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs @@ -14,8 +14,9 @@ using Umbraco.Core.Configuration.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Security; using Umbraco.Extensions; +using Umbraco.Infrastructure.Security; -namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice +namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.BackOffice { [TestFixture] public class BackOfficeClaimsPrincipalFactoryTests diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackOffice/BackOfficeLookupNormalizerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackOffice/BackOfficeLookupNormalizerTests.cs deleted file mode 100644 index 9ce3a8a3c3..0000000000 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackOffice/BackOfficeLookupNormalizerTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. - -using System; -using NUnit.Framework; -using Umbraco.Core.Security; - -namespace Umbraco.Tests.UnitTests.Umbraco.Web.Backoffice -{ - public class BackOfficeLookupNormalizerTests - { - [Test] - [TestCase(null)] - [TestCase("")] - [TestCase(" ")] - public void NormalizeName_When_Name_Null_Or_Whitespace_Expect_Same_Returned(string name) - { - var sut = new BackOfficeLookupNormalizer(); - - var normalizedName = sut.NormalizeName(name); - - Assert.AreEqual(name, normalizedName); - } - - [Test] - public void NormalizeName_Expect_Input_Returned() - { - var name = Guid.NewGuid().ToString(); - var sut = new BackOfficeLookupNormalizer(); - - var normalizedName = sut.NormalizeName(name); - - Assert.AreEqual(name, normalizedName); - } - - [Test] - [TestCase(null)] - [TestCase("")] - [TestCase(" ")] - public void NormalizeEmail_When_Name_Null_Or_Whitespace_Expect_Same_Returned(string email) - { - var sut = new BackOfficeLookupNormalizer(); - - var normalizedEmail = sut.NormalizeEmail(email); - - Assert.AreEqual(email, normalizedEmail); - } - - [Test] - public void NormalizeEmail_Expect_Input_Returned() - { - var email = $"{Guid.NewGuid()}@umbraco"; - var sut = new BackOfficeLookupNormalizer(); - - var normalizedEmail = sut.NormalizeEmail(email); - - Assert.AreEqual(email, normalizedEmail); - } - } -} diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserManagerTests.cs index b06b9d1a93..438f6d35bc 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserManagerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserManagerTests.cs @@ -69,7 +69,6 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Security _mockPasswordHasher.Object, userValidators, pwdValidators, - new BackOfficeLookupNormalizer(), new BackOfficeIdentityErrorDescriber(), _mockServiceProviders.Object, new Mock().Object, diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/NopLookupNormalizerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/NoOpLookupNormalizerTests.cs similarity index 78% rename from src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/NopLookupNormalizerTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/NoOpLookupNormalizerTests.cs index f4ea348892..86cb339625 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/NopLookupNormalizerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/NoOpLookupNormalizerTests.cs @@ -3,19 +3,17 @@ using System; using NUnit.Framework; -using Umbraco.Core.Security; +using Umbraco.Infrastructure.Security; -namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice +namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Security { - public class NopLookupNormalizerTests + public class NoOpLookupNormalizerTests { [Test] - [TestCase(null)] - [TestCase("")] - [TestCase(" ")] - public void NormalizeName_When_Name_Null_Or_Whitespace_Expect_Same_Returned(string name) + public void NormalizeName_Expect_Input_Returned() { - var sut = new BackOfficeLookupNormalizer(); + var name = Guid.NewGuid().ToString(); + var sut = new NoOpLookupNormalizer(); var normalizedName = sut.NormalizeName(name); @@ -23,10 +21,23 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice } [Test] - public void NormalizeName_Expect_Input_Returned() + public void NormalizeEmail_Expect_Input_Returned() { - var name = Guid.NewGuid().ToString(); - var sut = new BackOfficeLookupNormalizer(); + var email = $"{Guid.NewGuid()}@umbraco"; + var sut = new NoOpLookupNormalizer(); + + var normalizedEmail = sut.NormalizeEmail(email); + + Assert.AreEqual(email, normalizedEmail); + } + + [Test] + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + public void NormalizeName_When_Name_Null_Or_Whitespace_Expect_Same_Returned(string name) + { + var sut = new NoOpLookupNormalizer(); var normalizedName = sut.NormalizeName(name); @@ -39,18 +50,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice [TestCase(" ")] public void NormalizeEmail_When_Name_Null_Or_Whitespace_Expect_Same_Returned(string email) { - var sut = new BackOfficeLookupNormalizer(); - - var normalizedEmail = sut.NormalizeEmail(email); - - Assert.AreEqual(email, normalizedEmail); - } - - [Test] - public void NormalizeEmail_Expect_Input_Returned() - { - var email = $"{Guid.NewGuid()}@umbraco"; - var sut = new BackOfficeLookupNormalizer(); + var sut = new NoOpLookupNormalizer(); var normalizedEmail = sut.NormalizeEmail(email); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs index 1beab6257c..0537c73ea7 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs @@ -140,7 +140,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers string password = "fakepassword9aw89rnyco3938cyr^%&*()i8Y"; Mock.Get(umbracoMembersUserManager) - .Setup(x => x.GeneratePassword(It.IsAny())) + .Setup(x => x.HashPassword(It.IsAny())) .Returns(password); Mock.Get(umbracoMembersUserManager) .Setup(x => x.UpdateAsync(It.IsAny())) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/UsersControllerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/UsersControllerTests.cs index d8e5a04c59..ac835779a7 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/UsersControllerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/UsersControllerTests.cs @@ -5,6 +5,7 @@ using AutoFixture.NUnit3; using Moq; using NUnit.Framework; using Umbraco.Core.Security; +using Umbraco.Infrastructure.Security; using Umbraco.Tests.UnitTests.AutoFixture; using Umbraco.Web.BackOffice.Controllers; using Umbraco.Web.Common.Exceptions; diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index 502ffbcba2..77cd022d99 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -21,6 +21,7 @@ using Umbraco.Core.Models.Security; using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Extensions; +using Umbraco.Infrastructure.Security; using Umbraco.Net; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.BackOffice.Security; diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs index 34d3a96ca3..7939b0f306 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs @@ -24,6 +24,7 @@ using Umbraco.Core.Serialization; using Umbraco.Core.Services; using Umbraco.Core.WebAssets; using Umbraco.Extensions; +using Umbraco.Infrastructure.Security; using Umbraco.Web.BackOffice.ActionResults; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.BackOffice.Security; diff --git a/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs b/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs index d156551c26..a651a22075 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs @@ -20,6 +20,7 @@ using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Extensions; +using Umbraco.Infrastructure.Security; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.BackOffice.Security; using Umbraco.Web.Common.Attributes; diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index 957863704b..5efea5a1ca 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -435,7 +435,7 @@ namespace Umbraco.Web.BackOffice.Controllers throw HttpResponseException.CreateNotificationValidationErrorResponse(validatePassword.Errors.ToErrorMessage()); } - string newPassword = _memberManager.GeneratePassword(contentItem.Password.NewPassword); + string newPassword = _memberManager.HashPassword(contentItem.Password.NewPassword); identityMember.PasswordHash = newPassword; } diff --git a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs index fca8c49004..3f0bdcfb70 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs @@ -28,6 +28,7 @@ using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Extensions; +using Umbraco.Infrastructure.Security; using Umbraco.Web.BackOffice.ActionResults; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.BackOffice.ModelBinders; diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs index 7a40c03f9f..bca0c67ed7 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs @@ -67,7 +67,7 @@ namespace Umbraco.Web.BackOffice.DependencyInjection services.TryAddScoped, UserClaimsPrincipalFactory>(); // CUSTOM: - services.TryAddScoped(); + services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); services.TryAddSingleton(); @@ -79,7 +79,7 @@ namespace Umbraco.Web.BackOffice.DependencyInjection * To validate the container the following registrations are required (dependencies of UserManager) * Perhaps we shouldn't be registering UserManager at all and only registering/depending the UmbracoBackOffice prefixed types. */ - services.TryAddScoped(); + services.TryAddScoped(); services.TryAddScoped(); return new BackOfficeIdentityBuilder(services); diff --git a/src/Umbraco.Web.BackOffice/Filters/CheckIfUserTicketDataIsStaleAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/CheckIfUserTicketDataIsStaleAttribute.cs index 6e14468a2a..75fd435741 100644 --- a/src/Umbraco.Web.BackOffice/Filters/CheckIfUserTicketDataIsStaleAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/CheckIfUserTicketDataIsStaleAttribute.cs @@ -16,6 +16,7 @@ using Umbraco.Core.Models.Membership; using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Extensions; +using Umbraco.Infrastructure.Security; using Umbraco.Web.BackOffice.Security; using Umbraco.Web.Common.Security; diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficePasswordHasher.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficePasswordHasher.cs index 65f1a7f5bc..7c07699420 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficePasswordHasher.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficePasswordHasher.cs @@ -1,9 +1,10 @@ -using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity; using Umbraco.Core.Security; using Umbraco.Core; using Umbraco.Core.Models.Membership; using Microsoft.Extensions.Options; using Umbraco.Core.Serialization; +using Umbraco.Infrastructure.Security; namespace Umbraco.Web.BackOffice.Security { diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeSecurityStampValidator.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeSecurityStampValidator.cs index abd0af1353..ab22e0ba06 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeSecurityStampValidator.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeSecurityStampValidator.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Core.Security; +using Umbraco.Infrastructure.Security; using Umbraco.Web.Common.Security; namespace Umbraco.Web.BackOffice.Security diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeSessionIdValidator.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeSessionIdValidator.cs index 1ccb94e988..058ffd6585 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeSessionIdValidator.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeSessionIdValidator.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Configuration.Models; using Umbraco.Core.Hosting; using Umbraco.Core.Security; using Umbraco.Extensions; +using Umbraco.Infrastructure.Security; namespace Umbraco.Web.BackOffice.Security { diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs index 6d1c348d7f..3cbd562ef2 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; @@ -13,6 +13,7 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Security; using Umbraco.Extensions; +using Umbraco.Infrastructure.Security; using Umbraco.Net; using Umbraco.Web.BackOffice.Security; diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManagerAuditer.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManagerAuditer.cs index 81be953d22..5ba7a0fd56 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManagerAuditer.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManagerAuditer.cs @@ -6,6 +6,7 @@ using Umbraco.Core.Configuration.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Security; using Umbraco.Core.Services; +using Umbraco.Infrastructure.Security; namespace Umbraco.Web.Common.Security { diff --git a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeIdentityOptions.cs b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeIdentityOptions.cs index 989c852350..a9d4575c4d 100644 --- a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeIdentityOptions.cs +++ b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeIdentityOptions.cs @@ -6,6 +6,7 @@ using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Security; +using Umbraco.Infrastructure.Security; namespace Umbraco.Web.BackOffice.Security { diff --git a/src/Umbraco.Web.BackOffice/Security/ExternalSignInAutoLinkOptions.cs b/src/Umbraco.Web.BackOffice/Security/ExternalSignInAutoLinkOptions.cs index 8636d9e62d..149f5efb56 100644 --- a/src/Umbraco.Web.BackOffice/Security/ExternalSignInAutoLinkOptions.cs +++ b/src/Umbraco.Web.BackOffice/Security/ExternalSignInAutoLinkOptions.cs @@ -1,8 +1,9 @@ -using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity; using System; using System.Runtime.Serialization; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Security; +using Umbraco.Infrastructure.Security; using SecurityConstants = Umbraco.Core.Constants.Security; namespace Umbraco.Web.BackOffice.Security diff --git a/src/Umbraco.Web.BackOffice/Security/IBackOfficeSignInManager.cs b/src/Umbraco.Web.BackOffice/Security/IBackOfficeSignInManager.cs index 669ca21239..1065c117a1 100644 --- a/src/Umbraco.Web.BackOffice/Security/IBackOfficeSignInManager.cs +++ b/src/Umbraco.Web.BackOffice/Security/IBackOfficeSignInManager.cs @@ -1,9 +1,10 @@ -using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using System.Collections.Generic; using System.Security.Claims; using System.Threading.Tasks; using Umbraco.Core.Security; +using Umbraco.Infrastructure.Security; namespace Umbraco.Web.Common.Security { diff --git a/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs b/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs index 180f433fab..6144e7b63f 100644 --- a/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs +++ b/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs @@ -6,6 +6,7 @@ using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Security; using Umbraco.Extensions; +using Umbraco.Infrastructure.Security; using Umbraco.Web.Models; using IUser = Umbraco.Core.Models.Membership.IUser; diff --git a/src/Umbraco.Web.Common/Middleware/BootFailedMiddleware.cs b/src/Umbraco.Web.Common/Middleware/BootFailedMiddleware.cs index 685312778d..1dd3b65975 100644 --- a/src/Umbraco.Web.Common/Middleware/BootFailedMiddleware.cs +++ b/src/Umbraco.Web.Common/Middleware/BootFailedMiddleware.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Umbraco.Core; diff --git a/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs index 081ca6b581..ca1a3f561d 100644 --- a/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Configuration.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Security; using Umbraco.Extensions; +using Umbraco.Infrastructure.Security; using Umbraco.Net; using Umbraco.Web.Models.ContentEditing; @@ -28,13 +29,12 @@ namespace Umbraco.Web.Common.Security IPasswordHasher passwordHasher, IEnumerable> userValidators, IEnumerable> passwordValidators, - BackOfficeLookupNormalizer keyNormalizer, BackOfficeIdentityErrorDescriber errors, IServiceProvider services, IHttpContextAccessor httpContextAccessor, ILogger> logger, IOptions passwordConfiguration) - : base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger, passwordConfiguration) + : base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, errors, services, logger, passwordConfiguration) { _httpContextAccessor = httpContextAccessor; } diff --git a/src/Umbraco.Web.Common/Security/MembersUserManager.cs b/src/Umbraco.Web.Common/Security/MembersUserManager.cs index 86ea7b1972..1604c056b0 100644 --- a/src/Umbraco.Web.Common/Security/MembersUserManager.cs +++ b/src/Umbraco.Web.Common/Security/MembersUserManager.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Security.Principal; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; @@ -30,17 +28,16 @@ namespace Umbraco.Web.Common.Security IPasswordHasher passwordHasher, IEnumerable> userValidators, IEnumerable> passwordValidators, - //TODO: do we need members versions of this? - BackOfficeLookupNormalizer keyNormalizer, BackOfficeIdentityErrorDescriber errors, IServiceProvider services, IHttpContextAccessor httpContextAccessor, ILogger> logger, IOptions passwordConfiguration) - : base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger, passwordConfiguration) + : base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, errors, services, logger, passwordConfiguration) { _httpContextAccessor = httpContextAccessor; } + private string GetCurrentUserId(IPrincipal currentUser) { UmbracoBackOfficeIdentity umbIdentity = currentUser?.GetUmbracoIdentity(); @@ -55,82 +52,15 @@ namespace Umbraco.Web.Common.Security return new IdentityAuditEventArgs(auditEvent, ip, currentUserId, string.Empty, affectedUserId, affectedUsername); } + //TODO: have removed all other member audit events - can revisit if we need member auditing on a user level in future + public void RaiseForgotPasswordRequestedEvent(IPrincipal currentUser, string userId) => throw new NotImplementedException(); - // TODO: As per backoffice, review where these are raised and see if they can be simplified and either done in the this usermanager or the signin manager, - // lastly we'll resort to the authentication controller but we should try to remove all instances of that occuring - public void RaiseAccountLockedEvent(IPrincipal currentUser, string userId) => OnAccountLocked(CreateArgs(AuditEvent.AccountLocked, currentUser, userId, string.Empty)); + public void RaiseForgotPasswordChangedSuccessEvent(IPrincipal currentUser, string userId) => throw new NotImplementedException(); - public void RaiseAccountUnlockedEvent(IPrincipal currentUser, string userId) => OnAccountUnlocked(CreateArgs(AuditEvent.AccountUnlocked, currentUser, userId, string.Empty)); + public SignOutAuditEventArgs RaiseLogoutSuccessEvent(IPrincipal currentUser, string userId) => throw new NotImplementedException(); - public void RaiseForgotPasswordRequestedEvent(IPrincipal currentUser, string userId) => OnForgotPasswordRequested(CreateArgs(AuditEvent.ForgotPasswordRequested, currentUser, userId, string.Empty)); + public UserInviteEventArgs RaiseSendingUserInvite(IPrincipal currentUser, UserInvite invite, IUser createdUser) => throw new NotImplementedException(); - public void RaiseForgotPasswordChangedSuccessEvent(IPrincipal currentUser, string userId) => OnForgotPasswordChangedSuccess(CreateArgs(AuditEvent.ForgotPasswordChangedSuccess, currentUser, userId, string.Empty)); - - public void RaiseLoginFailedEvent(IPrincipal currentUser, string userId) => OnLoginFailed(CreateArgs(AuditEvent.LoginFailed, currentUser, userId, string.Empty)); - - public void RaiseLoginRequiresVerificationEvent(IPrincipal currentUser, string userId) => OnLoginRequiresVerification(CreateArgs(AuditEvent.LoginRequiresVerification, currentUser, userId, string.Empty)); - - public void RaiseLoginSuccessEvent(IPrincipal currentUser, string userId) => OnLoginSuccess(CreateArgs(AuditEvent.LoginSucces, currentUser, userId, string.Empty)); - - public SignOutAuditEventArgs RaiseLogoutSuccessEvent(IPrincipal currentUser, string userId) - { - var currentUserId = GetCurrentUserId(currentUser); - var args = new SignOutAuditEventArgs(AuditEvent.LogoutSuccess, IpResolver.GetCurrentRequestIpAddress(), performingUser: currentUserId, affectedUser: userId); - OnLogoutSuccess(args); - return args; - } - - public UserInviteEventArgs RaiseSendingUserInvite(IPrincipal currentUser, UserInvite invite, IUser createdUser) - { - var currentUserId = GetCurrentUserId(currentUser); - var ip = IpResolver.GetCurrentRequestIpAddress(); - var args = new UserInviteEventArgs(ip, currentUserId, invite, createdUser); - OnSendingUserInvite(args); - return args; - } - - public bool HasSendingUserInviteEventHandler => SendingUserInvite != null; - - // TODO: Comments re static events as per backofficeusermanager - public static event EventHandler AccountLocked; - public static event EventHandler AccountUnlocked; - public static event EventHandler ForgotPasswordRequested; - public static event EventHandler ForgotPasswordChangedSuccess; - public static event EventHandler LoginFailed; - public static event EventHandler LoginRequiresVerification; - public static event EventHandler LoginSuccess; - public static event EventHandler LogoutSuccess; - public static event EventHandler PasswordChanged; - public static event EventHandler PasswordReset; - public static event EventHandler ResetAccessFailedCount; - - /// - /// Raised when a user is invited - /// - public static event EventHandler SendingUserInvite; // this event really has nothing to do with the user manager but was the most convenient place to put it - - protected virtual void OnAccountLocked(IdentityAuditEventArgs e) => AccountLocked?.Invoke(this, e); - - protected virtual void OnSendingUserInvite(UserInviteEventArgs e) => SendingUserInvite?.Invoke(this, e); - - protected virtual void OnAccountUnlocked(IdentityAuditEventArgs e) => AccountUnlocked?.Invoke(this, e); - - protected virtual void OnForgotPasswordRequested(IdentityAuditEventArgs e) => ForgotPasswordRequested?.Invoke(this, e); - - protected virtual void OnForgotPasswordChangedSuccess(IdentityAuditEventArgs e) => ForgotPasswordChangedSuccess?.Invoke(this, e); - - protected virtual void OnLoginFailed(IdentityAuditEventArgs e) => LoginFailed?.Invoke(this, e); - - protected virtual void OnLoginRequiresVerification(IdentityAuditEventArgs e) => LoginRequiresVerification?.Invoke(this, e); - - protected virtual void OnLoginSuccess(IdentityAuditEventArgs e) => LoginSuccess?.Invoke(this, e); - - protected virtual void OnLogoutSuccess(SignOutAuditEventArgs e) => LogoutSuccess?.Invoke(this, e); - - protected virtual void OnPasswordChanged(IdentityAuditEventArgs e) => PasswordChanged?.Invoke(this, e); - - protected virtual void OnPasswordReset(IdentityAuditEventArgs e) => PasswordReset?.Invoke(this, e); - - protected virtual void OnResetAccessFailedCount(IdentityAuditEventArgs e) => ResetAccessFailedCount?.Invoke(this, e); + public bool HasSendingUserInviteEventHandler { get; } } } diff --git a/src/Umbraco.Web/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs b/src/Umbraco.Web/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs index 46b6540d73..6bba9af2e4 100644 --- a/src/Umbraco.Web/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs +++ b/src/Umbraco.Web/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Options; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Security; +using Umbraco.Infrastructure.Security; namespace Umbraco.Web.Security { diff --git a/src/Umbraco.Web/Security/IBackOfficeUserPasswordChecker.cs b/src/Umbraco.Web/Security/IBackOfficeUserPasswordChecker.cs index 7bd67e608a..c048903d0d 100644 --- a/src/Umbraco.Web/Security/IBackOfficeUserPasswordChecker.cs +++ b/src/Umbraco.Web/Security/IBackOfficeUserPasswordChecker.cs @@ -1,5 +1,6 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Umbraco.Core.Security; +using Umbraco.Infrastructure.Security; namespace Umbraco.Web.Security { diff --git a/src/Umbraco.Web/Security/Providers/MembersRoleProvider.cs b/src/Umbraco.Web/Security/Providers/MembersRoleProvider.cs index 89507a6c5b..da41838131 100644 --- a/src/Umbraco.Web/Security/Providers/MembersRoleProvider.cs +++ b/src/Umbraco.Web/Security/Providers/MembersRoleProvider.cs @@ -1,8 +1,6 @@ -using System.Collections.Specialized; using System.Configuration.Provider; using System.Linq; using System.Web.Security; -using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Services; @@ -13,6 +11,7 @@ namespace Umbraco.Web.Security.Providers public class MembersRoleProvider : RoleProvider { private readonly IMembershipRoleService _roleService; + private string _applicationName; public MembersRoleProvider(IMembershipRoleService roleService) { @@ -24,8 +23,6 @@ namespace Umbraco.Web.Security.Providers { } - private string _applicationName; - public override bool IsUserInRole(string username, string roleName) { return GetRolesForUser(username).Any(x => x == roleName); @@ -46,10 +43,12 @@ namespace Umbraco.Web.Security.Providers return _roleService.DeleteRole(roleName, throwOnPopulatedRole); } - public override bool RoleExists(string roleName) - { - return _roleService.GetAllRoles().Any(x => x == roleName); - } + /// + /// Returns true if the specified member role name exists + /// + /// Member role name + /// True if member role exists, otherwise false + public override bool RoleExists(string roleName) => _roleService.GetAllRolesTyped().Any(x => x.Name == roleName); public override void AddUsersToRoles(string[] usernames, string[] roleNames) { @@ -66,10 +65,11 @@ namespace Umbraco.Web.Security.Providers return _roleService.GetMembersInRole(roleName).Select(x => x.Username).ToArray(); } - public override string[] GetAllRoles() - { - return _roleService.GetAllRoles().ToArray(); - } + /// + /// Gets all the member roles + /// + /// A list of member roles + public override string[] GetAllRoles() => _roleService.GetAllRolesTyped().Select(x => x.Name).ToArray(); public override string[] FindUsersInRole(string roleName, string usernameToMatch) { @@ -87,6 +87,7 @@ namespace Umbraco.Web.Security.Providers { return _applicationName; } + set { if (string.IsNullOrEmpty(value)) From da8f8091facb71d9f0ad62c6ff5f1ffbb8619c41 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Mon, 11 Jan 2021 15:12:50 +0000 Subject: [PATCH 32/72] Namespace update. --- src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs | 4 ++-- src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs index a7d925e580..161be87677 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; +using Umbraco.Core; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Mapping; using Umbraco.Core.Models; @@ -15,9 +16,8 @@ using Umbraco.Core.Models.Identity; using Umbraco.Core.Models.Membership; using Umbraco.Core.Scoping; using Umbraco.Core.Services; -using Umbraco.Infrastructure.Security; -namespace Umbraco.Core.Security +namespace Umbraco.Infrastructure.Security { // TODO: Make this into a base class that can be re-used diff --git a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs index cac0d1ea78..b14cada788 100644 --- a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs @@ -49,7 +49,7 @@ namespace Umbraco.Infrastructure.Security /// public override bool SupportsQueryableUsers => false; // It would be nice to support this but we don't need to currently and that would require IQueryable support for our user service/repository - + /// /// Developers will need to override this to support custom 2 factor auth /// From 1a866a3b1eb5d05c5e5c3b1a813608478f8a9eb0 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Mon, 11 Jan 2021 17:01:44 +0000 Subject: [PATCH 33/72] Explicit types, braces style and expression bodies in the MemberService, and renamed rolestyped method. Namespace amendments. --- .../Services/IMembershipRoleService.cs | 2 +- .../Cache/DistributedCacheBinder_Handlers.cs | 1 + .../Compose/AuditEventsComponent.cs | 2 +- .../UmbracoBuilder.Services.cs | 2 +- .../PropertyEditorsComponent.cs | 1 + .../Security/MembersUserStore.cs | 2 +- .../Services/Implement/MemberService.cs | 155 ++++++++++-------- .../Services/MemberServiceTests.cs | 65 ++++---- .../Controllers/MemberController.cs | 1 + .../Security/Providers/MembersRoleProvider.cs | 4 +- 10 files changed, 123 insertions(+), 112 deletions(-) diff --git a/src/Umbraco.Core/Services/IMembershipRoleService.cs b/src/Umbraco.Core/Services/IMembershipRoleService.cs index e0d1e21e7a..a00d7e1b7f 100644 --- a/src/Umbraco.Core/Services/IMembershipRoleService.cs +++ b/src/Umbraco.Core/Services/IMembershipRoleService.cs @@ -9,7 +9,7 @@ namespace Umbraco.Core.Services where T : class, IMembershipUser { void AddRole(string roleName); - IEnumerable GetAllRolesTyped(); + IEnumerable GetAllRoles(); IEnumerable GetAllRoles(int memberId); IEnumerable GetAllRoles(string username); IEnumerable GetAllRolesIds(); diff --git a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs index 46968e81dc..b846d5467a 100644 --- a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs +++ b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs @@ -8,6 +8,7 @@ using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; using Umbraco.Core.Services.Changes; using Umbraco.Core.Services.Implement; +using Umbraco.Infrastructure.Services.Implement; namespace Umbraco.Web.Cache { diff --git a/src/Umbraco.Infrastructure/Compose/AuditEventsComponent.cs b/src/Umbraco.Infrastructure/Compose/AuditEventsComponent.cs index c085db2496..41c406065f 100644 --- a/src/Umbraco.Infrastructure/Compose/AuditEventsComponent.cs +++ b/src/Umbraco.Infrastructure/Compose/AuditEventsComponent.cs @@ -11,7 +11,7 @@ using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; using Umbraco.Extensions; - +using Umbraco.Infrastructure.Services.Implement; using Umbraco.Net; namespace Umbraco.Core.Compose diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs index 918bdcb941..ca7ba6e479 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs @@ -9,12 +9,12 @@ using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; using Umbraco.Core.DependencyInjection; -using Umbraco.Core.Events; using Umbraco.Core.Hosting; using Umbraco.Core.Packaging; using Umbraco.Core.Routing; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; +using Umbraco.Infrastructure.Services.Implement; namespace Umbraco.Infrastructure.DependencyInjection { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/PropertyEditorsComponent.cs b/src/Umbraco.Infrastructure/PropertyEditors/PropertyEditorsComponent.cs index cd7b7a1f39..4ad4d91283 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/PropertyEditorsComponent.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/PropertyEditorsComponent.cs @@ -7,6 +7,7 @@ using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; +using Umbraco.Infrastructure.Services.Implement; namespace Umbraco.Web.PropertyEditors { diff --git a/src/Umbraco.Infrastructure/Security/MembersUserStore.cs b/src/Umbraco.Infrastructure/Security/MembersUserStore.cs index c79e18c168..35816721c4 100644 --- a/src/Umbraco.Infrastructure/Security/MembersUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MembersUserStore.cs @@ -475,7 +475,7 @@ namespace Umbraco.Infrastructure.Security /// protected override Task> FindRoleAsync(string normalizedRoleName, CancellationToken cancellationToken) { - IMemberGroup group = _memberService.GetAllRolesTyped().SingleOrDefault(x => x.Name == normalizedRoleName); + IMemberGroup group = _memberService.GetAllRoles().SingleOrDefault(x => x.Name == normalizedRoleName); if (group == null) { return Task.FromResult((IdentityRole)null); diff --git a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs index 52f2fe0836..794da8230e 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs @@ -2,16 +2,17 @@ using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; -using Umbraco.Core.Composing; +using Umbraco.Core; using Umbraco.Core.Events; -using Umbraco.Core.Exceptions; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Scoping; +using Umbraco.Core.Services; +using Umbraco.Core.Services.Implement; -namespace Umbraco.Core.Services.Implement +namespace Umbraco.Infrastructure.Services.Implement { /// /// Represents the MemberService. @@ -928,24 +929,29 @@ namespace Umbraco.Core.Services.Implement scope.Complete(); } } - - /// - /// Returns a strongly typed list of all member groups - /// - /// - public IEnumerable GetAllRolesTyped() + /// + /// Returns a list of all member roles + /// + /// A list of member roles + + public IEnumerable GetAllRoles() { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); return _memberGroupRepository.GetMany().Select(x=>x).Distinct(); } } + /// + /// Returns a list of all member roles for a given member ID + /// + /// + /// A list of member roles public IEnumerable GetAllRoles(int memberId) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); var result = _memberGroupRepository.GetMemberGroupsForMember(memberId); @@ -955,17 +961,17 @@ namespace Umbraco.Core.Services.Implement public IEnumerable GetAllRoles(string username) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); - var result = _memberGroupRepository.GetMemberGroupsForMember(username); + IEnumerable result = _memberGroupRepository.GetMemberGroupsForMember(username); return result.Select(x => x.Name).Distinct(); } } public IEnumerable GetAllRolesIds() { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); return _memberGroupRepository.GetMany().Select(x => x.Id).Distinct(); @@ -974,27 +980,27 @@ namespace Umbraco.Core.Services.Implement public IEnumerable GetAllRolesIds(int memberId) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); - var result = _memberGroupRepository.GetMemberGroupsForMember(memberId); + IEnumerable result = _memberGroupRepository.GetMemberGroupsForMember(memberId); return result.Select(x => x.Id).Distinct(); } } public IEnumerable GetAllRolesIds(string username) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); - var result = _memberGroupRepository.GetMemberGroupsForMember(username); + IEnumerable result = _memberGroupRepository.GetMemberGroupsForMember(username); return result.Select(x => x.Id).Distinct(); } } public IEnumerable GetMembersInRole(string roleName) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); return _memberRepository.GetByMemberGroup(roleName); @@ -1003,7 +1009,7 @@ namespace Umbraco.Core.Services.Implement public IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); return _memberRepository.FindMembersInRole(roleName, usernameToMatch, matchType); @@ -1012,71 +1018,66 @@ namespace Umbraco.Core.Services.Implement public bool DeleteRole(string roleName, bool throwIfBeingUsed) { - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.MemberTree); if (throwIfBeingUsed) { // get members in role - var membersInRole = _memberRepository.GetByMemberGroup(roleName); + IEnumerable membersInRole = _memberRepository.GetByMemberGroup(roleName); if (membersInRole.Any()) + { throw new InvalidOperationException("The role " + roleName + " is currently assigned to members"); + } } - var query = Query().Where(g => g.Name == roleName); - var found = _memberGroupRepository.Get(query).ToArray(); + IQuery query = Query().Where(g => g.Name == roleName); + IMemberGroup[] found = _memberGroupRepository.Get(query).ToArray(); - foreach (var memberGroup in found) + foreach (IMemberGroup memberGroup in found) + { _memberGroupService.Delete(memberGroup); + } scope.Complete(); return found.Length > 0; } } - public void AssignRole(string username, string roleName) - { - AssignRoles(new[] { username }, new[] { roleName }); - } + public void AssignRole(string username, string roleName) => AssignRoles(new[] { username }, new[] { roleName }); public void AssignRoles(string[] usernames, string[] roleNames) { - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.MemberTree); - var ids = _memberGroupRepository.GetMemberIds(usernames); + int[] ids = _memberGroupRepository.GetMemberIds(usernames); _memberGroupRepository.AssignRoles(ids, roleNames); scope.Events.Dispatch(AssignedRoles, this, new RolesEventArgs(ids, roleNames), nameof(AssignedRoles)); scope.Complete(); } } - public void DissociateRole(string username, string roleName) - { - DissociateRoles(new[] { username }, new[] { roleName }); - } + public void DissociateRole(string username, string roleName) => DissociateRoles(new[] { username }, new[] { roleName }); public void DissociateRoles(string[] usernames, string[] roleNames) { - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.MemberTree); - var ids = _memberGroupRepository.GetMemberIds(usernames); + int[] ids = _memberGroupRepository.GetMemberIds(usernames); _memberGroupRepository.DissociateRoles(ids, roleNames); scope.Events.Dispatch(RemovedRoles, this, new RolesEventArgs(ids, roleNames), nameof(RemovedRoles)); scope.Complete(); } } - public void AssignRole(int memberId, string roleName) - { - AssignRoles(new[] { memberId }, new[] { roleName }); - } + public void AssignRole(int memberId, string roleName) => AssignRoles(new[] { memberId }, new[] { roleName }); public void AssignRoles(int[] memberIds, string[] roleNames) { - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.MemberTree); _memberGroupRepository.AssignRoles(memberIds, roleNames); @@ -1085,14 +1086,11 @@ namespace Umbraco.Core.Services.Implement } } - public void DissociateRole(int memberId, string roleName) - { - DissociateRoles(new[] { memberId }, new[] { roleName }); - } + public void DissociateRole(int memberId, string roleName) => DissociateRoles(new[] { memberId }, new[] { roleName }); public void DissociateRoles(int[] memberIds, string[] roleNames) { - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.MemberTree); _memberGroupRepository.DissociateRoles(memberIds, roleNames); @@ -1105,10 +1103,7 @@ namespace Umbraco.Core.Services.Implement #region Private Methods - private void Audit(AuditType type, int userId, int objectId, string message = null) - { - _auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetName(UmbracoObjectTypes.Member), message)); - } + private void Audit(AuditType type, int userId, int objectId, string message = null) => _auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetName(UmbracoObjectTypes.Member), message)); #endregion @@ -1163,12 +1158,15 @@ namespace Umbraco.Core.Services.Implement /// public MemberExportModel ExportMember(Guid key) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - var query = Query().Where(x => x.Key == key); - var member = _memberRepository.Get(query).FirstOrDefault(); + IQuery query = Query().Where(x => x.Key == key); + IMember member = _memberRepository.Get(query).FirstOrDefault(); - if (member == null) return null; + if (member == null) + { + return null; + } var model = new MemberExportModel { @@ -1192,11 +1190,14 @@ namespace Umbraco.Core.Services.Implement private static IEnumerable GetPropertyExportItems(IMember member) { - if (member == null) throw new ArgumentNullException(nameof(member)); + if (member == null) + { + throw new ArgumentNullException(nameof(member)); + } var exportProperties = new List(); - foreach (var property in member.Properties) + foreach (IProperty property in member.Properties) { var propertyExportModel = new MemberExportProperty { @@ -1224,15 +1225,14 @@ namespace Umbraco.Core.Services.Implement public void DeleteMembersOfType(int memberTypeId) { // note: no tree to manage here - - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.MemberTree); // TODO: What about content that has the contenttype as part of its composition? - var query = Query().Where(x => x.ContentTypeId == memberTypeId); + IQuery query = Query().Where(x => x.ContentTypeId == memberTypeId); - var members = _memberRepository.Get(query).ToArray(); + IMember[] members = _memberRepository.Get(query).ToArray(); var deleteEventArgs = new DeleteEventArgs(members); if (scope.Events.DispatchCancelable(Deleting, this, deleteEventArgs)) @@ -1241,43 +1241,58 @@ namespace Umbraco.Core.Services.Implement return; } - foreach (var member in members) + foreach (IMember member in members) { // delete media // triggers the deleted event (and handles the files) DeleteLocked(scope, member); } + scope.Complete(); } } private IMemberType GetMemberType(IScope scope, string memberTypeAlias) { - if (memberTypeAlias == null) throw new ArgumentNullException(nameof(memberTypeAlias)); - if (string.IsNullOrWhiteSpace(memberTypeAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(memberTypeAlias)); + if (memberTypeAlias == null) + { + throw new ArgumentNullException(nameof(memberTypeAlias)); + } + + if (string.IsNullOrWhiteSpace(memberTypeAlias)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(memberTypeAlias)); + } scope.ReadLock(Constants.Locks.MemberTypes); - var memberType = _memberTypeRepository.Get(memberTypeAlias); + IMemberType memberType = _memberTypeRepository.Get(memberTypeAlias); if (memberType == null) + { throw new Exception($"No MemberType matching the passed in Alias: '{memberTypeAlias}' was found"); // causes rollback + } return memberType; } private IMemberType GetMemberType(string memberTypeAlias) { - if (memberTypeAlias == null) throw new ArgumentNullException(nameof(memberTypeAlias)); - if (string.IsNullOrWhiteSpace(memberTypeAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(memberTypeAlias)); + if (memberTypeAlias == null) + { + throw new ArgumentNullException(nameof(memberTypeAlias)); + } - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + if (string.IsNullOrWhiteSpace(memberTypeAlias)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(memberTypeAlias)); + } + + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { return GetMemberType(scope, memberTypeAlias); } } - - #endregion } } diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs index a286b0ec42..59ae47a0f9 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs @@ -12,6 +12,7 @@ using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Tests.Common; using Umbraco.Tests.Common.Builders; @@ -28,6 +29,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services public class MemberServiceTests : UmbracoIntegrationTest { private IMemberTypeService MemberTypeService => GetRequiredService(); + private IMemberService MemberService => GetRequiredService(); [SetUp] @@ -64,11 +66,11 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services [Test] public void Can_Get_By_Username() { - var memberType = MemberTypeService.Get("member"); + IMemberType memberType = MemberTypeService.Get("member"); IMember member = new Member("xname", "xemail", "xusername", "xrawpassword", memberType, true); MemberService.Save(member); - var member2 = MemberService.GetByUsername(member.Username); + IMember member2 = MemberService.GetByUsername(member.Username); Assert.IsNotNull(member2); Assert.AreEqual(member.Email, member2.Email); @@ -77,8 +79,8 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services [Test] public void Can_Set_Last_Login_Date() { - var now = DateTime.Now; - var memberType = MemberTypeService.Get("member"); + DateTime now = DateTime.Now; + IMemberType memberType = MemberTypeService.Get("member"); IMember member = new Member("xname", "xemail", "xusername", "xrawpassword", memberType, true) { LastLoginDate = now, @@ -86,7 +88,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services }; MemberService.Save(member); - var newDate = now.AddDays(10); + DateTime newDate = now.AddDays(10); MemberService.SetLastLogin(member.Username, newDate); //re-get @@ -99,7 +101,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services [Test] public void Can_Create_Member_With_Properties() { - var memberType = MemberTypeService.Get("member"); + IMemberType memberType = MemberTypeService.Get("member"); IMember member = new Member("xname", "xemail", "xusername", "xrawpassword", memberType, true); MemberService.Save(member); @@ -111,7 +113,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services var publishedSnapshotAccessor = new TestPublishedSnapshotAccessor(); var variationContextAccessor = new TestVariationContextAccessor(); - var pmember = PublishedMember.Create(member, pmemberType, false, publishedSnapshotAccessor, variationContextAccessor, GetRequiredService()); + IPublishedContent pmember = PublishedMember.Create(member, pmemberType, false, publishedSnapshotAccessor, variationContextAccessor, GetRequiredService()); // contains the umbracoMember... properties created when installing, on the member type // contains the other properties, that PublishedContentType adds (BuiltinMemberProperties) @@ -142,7 +144,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services Assert.IsTrue(properties.Select(x => x.Alias).ContainsAll(aliases)); - var email = properties[aliases.IndexOf(nameof(IMember.Email))]; + IPublishedProperty email = properties[aliases.IndexOf(nameof(IMember.Email))]; Assert.AreEqual("xemail", email.GetSourceValue()); } @@ -155,7 +157,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services MemberService.Save(member); Assert.AreNotEqual(0, member.Id); - var foundMember = MemberService.GetById(member.Id); + IMember foundMember = MemberService.GetById(member.Id); Assert.IsNotNull(foundMember); Assert.AreEqual("test@test.com", foundMember.Email); } @@ -169,7 +171,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services MemberService.Save(member); Assert.AreNotEqual(0, member.Id); - var foundMember = MemberService.GetById(member.Id); + IMember foundMember = MemberService.GetById(member.Id); Assert.IsNotNull(foundMember); Assert.AreEqual("test@test.marketing", foundMember.Email); } @@ -179,7 +181,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services { MemberService.AddRole("MyTestRole"); - IEnumerable found = MemberService.GetAllRolesTyped(); + IEnumerable found = MemberService.GetAllRoles(); Assert.AreEqual(1, found.Count()); Assert.AreEqual("MyTestRole", found.Single()); @@ -191,7 +193,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services MemberService.AddRole("MyTestRole"); MemberService.AddRole("MyTestRole"); - IEnumerable found = MemberService.GetAllRolesTyped(); + IEnumerable found = MemberService.GetAllRoles(); Assert.AreEqual(1, found.Count()); Assert.AreEqual("MyTestRole", found.Single()); @@ -204,19 +206,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services MemberService.AddRole("MyTestRole2"); MemberService.AddRole("MyTestRole3"); - IEnumerable found = MemberService.GetAllRolesTyped(); - - Assert.AreEqual(3, found.Count()); - } - - [Test] - public void Can_Get_All_Roles_Typed() - { - MemberService.AddRole("MyTestRole1"); - MemberService.AddRole("MyTestRole2"); - MemberService.AddRole("MyTestRole3"); - - var found = MemberService.GetAllRolesTyped(); + IEnumerable found = MemberService.GetAllRoles(); Assert.AreEqual(3, found.Count()); } @@ -228,10 +218,11 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services MemberService.AddRole("MyTestRole2"); MemberService.AddRole("MyTestRole3"); - var found = MemberService.GetAllRolesIds(); + IEnumerable found = MemberService.GetAllRolesIds(); Assert.AreEqual(3, found.Count()); } + [Test] public void Can_Get_All_Roles_By_Member_Id() { @@ -245,11 +236,12 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services MemberService.AddRole("MyTestRole3"); MemberService.AssignRoles(new[] { member.Id }, new[] { "MyTestRole1", "MyTestRole2" }); - var memberRoles = MemberService.GetAllRoles(member.Id); + IEnumerable memberRoles = MemberService.GetAllRoles(member.Id); Assert.AreEqual(2, memberRoles.Count()); } + [Test] public void Can_Get_All_Roles_Ids_By_Member_Id() { @@ -263,11 +255,12 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services MemberService.AddRole("MyTestRole3"); MemberService.AssignRoles(new[] { member.Id }, new[] { "MyTestRole1", "MyTestRole2" }); - var memberRoles = MemberService.GetAllRolesIds(member.Id); + IEnumerable memberRoles = MemberService.GetAllRolesIds(member.Id); Assert.AreEqual(2, memberRoles.Count()); } + [Test] public void Can_Get_All_Roles_By_Member_Username() { @@ -284,10 +277,10 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services MemberService.AddRole("MyTestRole3"); MemberService.AssignRoles(new[] { member.Id, member2.Id }, new[] { "MyTestRole1", "MyTestRole2" }); - var memberRoles = MemberService.GetAllRoles("test"); + IEnumerable memberRoles = MemberService.GetAllRoles("test"); Assert.AreEqual(2, memberRoles.Count()); - var memberRoles2 = MemberService.GetAllRoles("test2@test.com"); + IEnumerable memberRoles2 = MemberService.GetAllRoles("test2@test.com"); Assert.AreEqual(2, memberRoles2.Count()); } @@ -298,7 +291,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services MemberService.DeleteRole("MyTestRole1", false); - IEnumerable memberRoles = MemberService.GetAllRolesTyped(); + IEnumerable memberRoles = MemberService.GetAllRoles(); Assert.AreEqual(0, memberRoles.Count()); } @@ -322,7 +315,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services { MemberService.AddRole("MyTestRole1"); int roleId; - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { roleId = scope.Database.ExecuteScalar("SELECT id from umbracoNode where [text] = 'MyTestRole1'"); scope.Complete(); @@ -330,19 +323,19 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services IMemberType memberType = MemberTypeBuilder.CreateSimpleMemberType(); MemberTypeService.Save(memberType); - var member1 = MemberBuilder.CreateSimpleMember(memberType, "test1", "test1@test.com", "pass", "test1"); + Member member1 = MemberBuilder.CreateSimpleMember(memberType, "test1", "test1@test.com", "pass", "test1"); MemberService.Save(member1); - var member2 = MemberBuilder.CreateSimpleMember(memberType, "test2", "test2@test.com", "pass", "test2"); + Member member2 = MemberBuilder.CreateSimpleMember(memberType, "test2", "test2@test.com", "pass", "test2"); MemberService.Save(member2); - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { scope.Database.Insert(new Member2MemberGroupDto { MemberGroup = roleId, Member = member1.Id }); scope.Database.Insert(new Member2MemberGroupDto { MemberGroup = roleId, Member = member2.Id }); scope.Complete(); } - var membersInRole = MemberService.GetMembersInRole("MyTestRole1"); + IEnumerable membersInRole = MemberService.GetMembersInRole("MyTestRole1"); Assert.AreEqual(2, membersInRole.Count()); } diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index 5efea5a1ca..12622bf4f6 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -26,6 +26,7 @@ using Umbraco.Core.Services.Implement; using Umbraco.Core.Strings; using Umbraco.Extensions; using Umbraco.Infrastructure.Security; +using Umbraco.Infrastructure.Services.Implement; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.BackOffice.ModelBinders; using Umbraco.Web.Common.Attributes; diff --git a/src/Umbraco.Web/Security/Providers/MembersRoleProvider.cs b/src/Umbraco.Web/Security/Providers/MembersRoleProvider.cs index da41838131..0637b08621 100644 --- a/src/Umbraco.Web/Security/Providers/MembersRoleProvider.cs +++ b/src/Umbraco.Web/Security/Providers/MembersRoleProvider.cs @@ -48,7 +48,7 @@ namespace Umbraco.Web.Security.Providers /// /// Member role name /// True if member role exists, otherwise false - public override bool RoleExists(string roleName) => _roleService.GetAllRolesTyped().Any(x => x.Name == roleName); + public override bool RoleExists(string roleName) => _roleService.GetAllRoles().Any(x => x.Name == roleName); public override void AddUsersToRoles(string[] usernames, string[] roleNames) { @@ -69,7 +69,7 @@ namespace Umbraco.Web.Security.Providers /// Gets all the member roles /// /// A list of member roles - public override string[] GetAllRoles() => _roleService.GetAllRolesTyped().Select(x => x.Name).ToArray(); + public override string[] GetAllRoles() => _roleService.GetAllRoles().Select(x => x.Name).ToArray(); public override string[] FindUsersInRole(string roleName, string usernameToMatch) { From 5b4f9a5100262a46109c18cdabb4e4b82f35c954 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Mon, 11 Jan 2021 17:03:34 +0000 Subject: [PATCH 34/72] Separated properties by a line --- .../Services/IMembershipRoleService.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Services/IMembershipRoleService.cs b/src/Umbraco.Core/Services/IMembershipRoleService.cs index a00d7e1b7f..b44bc53dac 100644 --- a/src/Umbraco.Core/Services/IMembershipRoleService.cs +++ b/src/Umbraco.Core/Services/IMembershipRoleService.cs @@ -5,26 +5,43 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Services { - public interface IMembershipRoleService + public interface IMembershipRoleService where T : class, IMembershipUser { void AddRole(string roleName); + IEnumerable GetAllRoles(); + IEnumerable GetAllRoles(int memberId); + IEnumerable GetAllRoles(string username); + IEnumerable GetAllRolesIds(); + IEnumerable GetAllRolesIds(int memberId); + IEnumerable GetAllRolesIds(string username); + IEnumerable GetMembersInRole(string roleName); + IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith); + bool DeleteRole(string roleName, bool throwIfBeingUsed); + void AssignRole(string username, string roleName); + void AssignRoles(string[] usernames, string[] roleNames); + void DissociateRole(string username, string roleName); + void DissociateRoles(string[] usernames, string[] roleNames); + void AssignRole(int memberId, string roleName); + void AssignRoles(int[] memberIds, string[] roleNames); + void DissociateRole(int memberId, string roleName); + void DissociateRoles(int[] memberIds, string[] roleNames); } } From 6dd5ae910a0b3ef1a1619b7f5f880c985d361858 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Wed, 20 Jan 2021 17:42:31 +0000 Subject: [PATCH 35/72] Fixed tests, merged latest --- .../Umbraco.Infrastructure/Services/MemberServiceTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs index 723d4d377e..01eb00bdec 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs @@ -186,7 +186,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services IEnumerable found = MemberService.GetAllRoles(); Assert.AreEqual(1, found.Count()); - Assert.AreEqual("MyTestRole", found.Single()); + Assert.AreEqual("MyTestRole", found.Single().Name); } [Test] @@ -198,7 +198,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services IEnumerable found = MemberService.GetAllRoles(); Assert.AreEqual(1, found.Count()); - Assert.AreEqual("MyTestRole", found.Single()); + Assert.AreEqual("MyTestRole", found.Single().Name); } [Test] From 74efd7fbda13cf680f86941f58f6bbc782ed9fb9 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 22 Jan 2021 09:20:46 +0100 Subject: [PATCH 36/72] Fixed issues with GetTreeNode --- .../Trees/MemberTreeController.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs index 4d5b204ed5..bd9373cdfe 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs @@ -55,13 +55,14 @@ namespace Umbraco.Web.BackOffice.Trees /// /// Gets an individual tree node /// - /// - /// - /// - public ActionResult GetTreeNode(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings) + public ActionResult GetTreeNode([FromRoute]string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings) { - //TODO: this is currently throwing an exception when loading a member - var node = GetSingleTreeNode(id, queryStrings); + ActionResult node = GetSingleTreeNode(id, queryStrings); + + if (!(node.Result is null)) + { + return node.Result; + } //add the tree alias to the node since it is standalone (has no root for which this normally belongs) node.Value.AdditionalData["treeAlias"] = TreeAlias; From c7725953f377bf46c1df7abb0e828d33a7be208d Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Tue, 26 Jan 2021 11:52:32 +0000 Subject: [PATCH 37/72] Remove TODOs since have been covered by other tickets --- src/Umbraco.Infrastructure/Search/ExamineComponent.cs | 3 +-- .../Security/IdentityMapDefinition.cs | 7 +++---- src/Umbraco.Infrastructure/Security/MembersUserStore.cs | 2 -- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Infrastructure/Search/ExamineComponent.cs b/src/Umbraco.Infrastructure/Search/ExamineComponent.cs index 18120a9f32..a5fc5253f2 100644 --- a/src/Umbraco.Infrastructure/Search/ExamineComponent.cs +++ b/src/Umbraco.Infrastructure/Search/ExamineComponent.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -513,7 +513,6 @@ namespace Umbraco.Web.Search DeferedReIndexForContent.Execute(_taskHelper, this, sender, isPublished); } - //TODO: this causes an exception when deleting a member private void ReIndexForMember(IMember member) { var actions = DeferedActions.Get(_scopeProvider); diff --git a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs index 88dd6eaeb7..e79d346c8a 100644 --- a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs +++ b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs @@ -104,11 +104,10 @@ namespace Umbraco.Infrastructure.Security target.IsApproved = source.IsApproved; //target.SecurityStamp = source.SecurityStamp; target.LockoutEnd = source.IsLockedOut ? DateTime.MaxValue.ToUniversalTime() : (DateTime?)null; + + // NB: same comments re AutoMapper as per BackOfficeUser } - private static string GetPasswordHash(string storedPass) - { - return storedPass.StartsWith(Constants.Security.EmptyPasswordPrefix) ? null : storedPass; - } + private static string GetPasswordHash(string storedPass) => storedPass.StartsWith(Constants.Security.EmptyPasswordPrefix) ? null : storedPass; } } diff --git a/src/Umbraco.Infrastructure/Security/MembersUserStore.cs b/src/Umbraco.Infrastructure/Security/MembersUserStore.cs index 35816721c4..c90a98de18 100644 --- a/src/Umbraco.Infrastructure/Security/MembersUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MembersUserStore.cs @@ -65,8 +65,6 @@ namespace Umbraco.Infrastructure.Security } // create member - // TODO: are we keeping this method? The user service creates the member directly - // but this way we get the member type by alias first IMember memberEntity = _memberService.CreateMember( user.UserName, user.Email, From 9b9a3d9197a04552af387b0318738e7878cb4356 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Tue, 26 Jan 2021 11:55:12 +0000 Subject: [PATCH 38/72] Remove todo since bug is not happening --- src/Umbraco.Web.BackOffice/Controllers/MemberController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index 49589efbd9..0cc4ea79b3 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -190,7 +190,6 @@ namespace Umbraco.Web.BackOffice.Controllers [OutgoingEditorModelEvent] public MemberDisplay GetByKey(Guid key) { - // TODO: this is not finding the key currently after member creation IMember foundMember = _memberService.GetByKey(key); if (foundMember == null) { From 90899c68f8b2904a766257a9dbe3cf479136df0f Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Tue, 26 Jan 2021 11:58:02 +0000 Subject: [PATCH 39/72] Removed todos --- src/Umbraco.Web.BackOffice/Controllers/MemberController.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index 0cc4ea79b3..f44909cd35 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -276,8 +276,7 @@ namespace Umbraco.Web.BackOffice.Controllers return NotFound(); } - // TODO: There's 3 things saved here and we should do this all in one transaction, - // which we can do here by wrapping in a scope + // TODO: There's 3 things saved here and we should do this all in one transaction, which we can do here by wrapping in a scope // but it would be nicer to have this taken care of within the Save method itself // return the updated model @@ -469,7 +468,6 @@ namespace Umbraco.Web.BackOffice.Controllers if (contentItem.Password != null && !contentItem.Password.NewPassword.IsNullOrWhiteSpace()) { - // TODO: this currently stops the user interacting with the client-side when invalid IdentityResult validPassword = await _memberManager.ValidatePasswordAsync(contentItem.Password.NewPassword); if (!validPassword.Succeeded) { From ce43c979b575dc382e81cb00bc69e786b151726d Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Thu, 28 Jan 2021 11:35:02 +0000 Subject: [PATCH 40/72] Added MemberController unit tests. Removed TODO. --- .../Models/Mapping/MemberMapDefinition.cs | 12 +- .../Security/BackOfficeIdentityUser.cs | 8 +- .../Security/MembersIdentityUser.cs | 40 +-- .../Security/MembersUserStore.cs | 4 - .../Security/MemberIdentityUserStoreTests.cs | 31 +- .../Controllers/MemberControllerUnitTests.cs | 298 +++++++++++++----- .../Controllers/MemberController.cs | 5 +- 7 files changed, 260 insertions(+), 138 deletions(-) diff --git a/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs index c17ef25c43..a69d40c291 100644 --- a/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs @@ -9,17 +9,23 @@ namespace Umbraco.Core.Models.Mapping /// public void DefineMaps(UmbracoMapper mapper) => mapper.Define(Map); - //TODO: put this here instead of a new mapper definition (like user). Can move - private static void Map(MemberSave source, IMember target, MapperContext context) { - // TODO: ensure all properties are mapped as required target.IsApproved = source.IsApproved; target.Name = source.Name; target.Email = source.Email; target.Key = source.Key; target.Username = source.Username; target.Comments = source.Comments; + target.CreateDate = source.CreateDate; + target.UpdateDate = source.UpdateDate; + target.Email = source.Email; + + // TODO: ensure all properties are mapped as required + //target.Id = source.Id; + //target.ParentId = -1; + //target.Path = "-1," + source.Id; + //TODO: add groups as required } } diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs index ae9b351b65..740777f820 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs @@ -93,7 +93,9 @@ namespace Umbraco.Infrastructure.Security set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); } - + /// + /// Gets or sets the password config + /// public string PasswordConfig { get => _passwordConfig; @@ -186,13 +188,13 @@ namespace Umbraco.Infrastructure.Security { get { - var isLocked = LockoutEnd.HasValue && LockoutEnd.Value.ToLocalTime() >= DateTime.Now; + bool isLocked = LockoutEnd.HasValue && LockoutEnd.Value.ToLocalTime() >= DateTime.Now; return isLocked; } } /// - /// Gets or sets a value indicating the IUser IsApproved + /// Gets or sets a value indicating whether the IUser IsApproved /// public bool IsApproved { get; set; } diff --git a/src/Umbraco.Infrastructure/Security/MembersIdentityUser.cs b/src/Umbraco.Infrastructure/Security/MembersIdentityUser.cs index e7b7450704..66ed733315 100644 --- a/src/Umbraco.Infrastructure/Security/MembersIdentityUser.cs +++ b/src/Umbraco.Infrastructure/Security/MembersIdentityUser.cs @@ -17,18 +17,28 @@ namespace Umbraco.Infrastructure.Security private string _passwordConfig; private IReadOnlyCollection _groups; - // TODO: reused from backoffice - share? // Custom comparer for enumerables private static readonly DelegateEqualityComparer> s_groupsComparer = new DelegateEqualityComparer>( (groups, enumerable) => groups.Select(x => x.Alias).UnsortedSequenceEqual(enumerable.Select(x => x.Alias)), groups => groups.GetHashCode()); + /// + /// Initializes a new instance of the class. + /// + public MembersIdentityUser(int userId) + { + // use the property setters - they do more than just setting a field + Id = UserIdToString(userId); + } + + public MembersIdentityUser() + { + } /// /// Used to construct a new instance without an identity /// - public static MembersIdentityUser CreateNew(string username, string email, string memberTypeAlias, - string name = null) + public static MembersIdentityUser CreateNew(string username, string email, string memberTypeAlias, string name = null) { if (string.IsNullOrWhiteSpace(username)) { @@ -46,20 +56,7 @@ namespace Umbraco.Infrastructure.Security user.EnableChangeTracking(); return user; } - - /// - /// Initializes a new instance of the class. - /// - public MembersIdentityUser(int userId) - { - // use the property setters - they do more than just setting a field - Id = UserIdToString(userId); - } - - public MembersIdentityUser() - { - } - + /// /// Gets or sets the member's real name /// @@ -80,7 +77,6 @@ namespace Umbraco.Infrastructure.Security /// /// Gets or sets the user groups - /// TBC: how to implement for members? /// public IReadOnlyCollection Groups { @@ -113,13 +109,13 @@ namespace Umbraco.Infrastructure.Security { get { - var isLocked = LockoutEnd.HasValue && LockoutEnd.Value.ToLocalTime() >= DateTime.Now; + bool isLocked = LockoutEnd.HasValue && LockoutEnd.Value.ToLocalTime() >= DateTime.Now; return isLocked; } } /// - /// Gets or sets a value indicating the member is approved + /// Gets or sets a value indicating whether the member is approved /// public bool IsApproved { get; set; } @@ -129,9 +125,5 @@ namespace Umbraco.Infrastructure.Security public string MemberTypeAlias { get; set; } private static string UserIdToString(int userId) => string.Intern(userId.ToString()); - - // TODO: implement as per base identity user - //public bool LoginsChanged; - //public bool RolesChanged; } } diff --git a/src/Umbraco.Infrastructure/Security/MembersUserStore.cs b/src/Umbraco.Infrastructure/Security/MembersUserStore.cs index c90a98de18..b75eb19beb 100644 --- a/src/Umbraco.Infrastructure/Security/MembersUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MembersUserStore.cs @@ -515,8 +515,6 @@ namespace Umbraco.Infrastructure.Security : user.SecurityStamp); } - // TODO: share all possible between backoffice user - private MembersIdentityUser AssignLoginsCallback(MembersIdentityUser user) { if (user != null) @@ -673,8 +671,6 @@ namespace Umbraco.Infrastructure.Security [EditorBrowsable(EditorBrowsableState.Never)] public override Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - // TODO: We should support these - /// /// Not supported in Umbraco /// diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs index e32cec4e89..335d85f411 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs @@ -33,14 +33,14 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Security [Test] public void GivenICreateUser_AndTheUserIsNull_ThenIShouldGetAFailedResultAsync() { - //arrange + // arrange MembersUserStore sut = CreateSut(); CancellationToken fakeCancellationToken = new CancellationToken(){}; - //act + // act Action actual = () => sut.CreateAsync(null, fakeCancellationToken); - //assert + // assert Assert.That(actual, Throws.ArgumentNullException); } @@ -48,10 +48,10 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Security [Test] public async Task GivenICreateANewUser_AndTheUserIsPopulatedCorrectly_ThenIShouldGetASuccessResultAsync() { - //arrange + // arrange MembersUserStore sut = CreateSut(); - MembersIdentityUser fakeUser = new MembersIdentityUser() { }; - CancellationToken fakeCancellationToken = new CancellationToken() { }; + var fakeUser = new MembersIdentityUser() { }; + var fakeCancellationToken = new CancellationToken() { }; IMemberType fakeMemberType = new MemberType(new MockShortStringHelper(), 77); IMember mockMember = Mock.Of(m => @@ -67,30 +67,15 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Security _mockMemberService.Setup(x => x.CreateMember(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(mockMember); _mockMemberService.Setup(x => x.Save(mockMember, raiseEvents)); - //act + // act IdentityResult identityResult = await sut.CreateAsync(fakeUser, fakeCancellationToken); - //assert + // assert Assert.IsTrue(identityResult.Succeeded); Assert.IsTrue(!identityResult.Errors.Any()); } - //FindByNameAsync - [Test] - public async Task GivenIGetUserNameAsync() - { - } - - [Test] - public async Task GivenIFindByNameAsync() - { - } - - //SetNormalizedUserNameAsync - //SetUserNameAsync - //HasPasswordAsync //GetPasswordHashAsync - //SetPasswordHashAsync //GetUserIdAsync } } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs index dfeb452d99..b0d4811cf6 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs @@ -1,38 +1,53 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using AngleSharp.Common; using AutoFixture.NUnit3; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; +using Umbraco.Core; using Umbraco.Core.Cache; +using Umbraco.Core.Configuration.Models; using Umbraco.Core.Dictionary; using Umbraco.Core.Events; using Umbraco.Core.Mapping; using Umbraco.Core.Models; +using Umbraco.Core.Models.ContentEditing; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.Validators; using Umbraco.Core.Security; using Umbraco.Core.Serialization; using Umbraco.Core.Services; using Umbraco.Core.Strings; -using Umbraco.Extensions; using Umbraco.Infrastructure.Security; +using Umbraco.Tests.Common.Builders; using Umbraco.Tests.UnitTests.AutoFixture; using Umbraco.Tests.UnitTests.Umbraco.Core.ShortStringHelper; using Umbraco.Web; using Umbraco.Web.BackOffice.Controllers; +using Umbraco.Web.BackOffice.Mapping; using Umbraco.Web.Common.ActionsResults; +using Umbraco.Web.ContentApps; using Umbraco.Web.Models; using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Models.Mapping; +using Umbraco.Web.PropertyEditors; +using IHostingEnvironment = Umbraco.Core.Hosting.IHostingEnvironment; namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers { [TestFixture] public class MemberControllerUnitTests { + private UmbracoMapper _mapper; + [Test] [AutoMoqData] public void PostSaveMember_WhenMemberIsNull_ExpectFailureResponse( @@ -50,16 +65,15 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers [AutoMoqData] public void PostSaveMember_WhenModelStateIsNotValid_ExpectFailureResponse( [Frozen] IMembersUserManager umbracoMembersUserManager, - IMemberTypeService memberTypeService, - IDataTypeService dataTypeService, IMemberService memberService, - MapDefinitionCollection memberMapDefinition, - PropertyEditorCollection propertyEditorCollection, + IMemberTypeService memberTypeService, + IMemberGroupService memberGroupService, + IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) { // arrange - Member member = SetupMemberTestData(memberMapDefinition, out UmbracoMapper mapper, out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew); - MemberController sut = CreateSut(mapper, memberService, memberTypeService, umbracoMembersUserManager, dataTypeService, propertyEditorCollection, backOfficeSecurityAccessor); + Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew); + MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor); sut.ModelState.AddModelError("key", "Invalid model state"); Mock.Get(umbracoMembersUserManager) @@ -87,16 +101,15 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers [AutoMoqData] public async Task PostSaveMember_SaveNew_WhenAllIsSetupCorrectly_ExpectSuccessResponse( [Frozen] IMembersUserManager umbracoMembersUserManager, - IMemberTypeService memberTypeService, - IDataTypeService dataTypeService, IMemberService memberService, - MapDefinitionCollection memberMapDefinition, - PropertyEditorCollection propertyEditorCollection, + IMemberTypeService memberTypeService, + IMemberGroupService memberGroupService, + IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IBackOfficeSecurity backOfficeSecurity) { // arrange - Member member = SetupMemberTestData(memberMapDefinition, out UmbracoMapper mapper, out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew); + Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew); Mock.Get(umbracoMembersUserManager) .Setup(x => x.CreateAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(() => IdentityResult.Success); @@ -109,31 +122,32 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers x => x.GetByEmail(It.IsAny())) .Returns(() => null) .Returns(() => member); + Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny())).Returns(() => member); - MemberController sut = CreateSut(mapper, memberService, memberTypeService, umbracoMembersUserManager, dataTypeService, propertyEditorCollection, backOfficeSecurityAccessor); + MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor); // act ActionResult result = await sut.PostSave(fakeMemberData); // assert Assert.IsNull(result.Result); - Assert.AreEqual(memberDisplay, result.Value); + Assert.IsNotNull(result.Value); + AssertMemberDisplayPropertiesAreEqual(memberDisplay, result.Value); } [Test] [AutoMoqData] - public async Task PostSaveMember_Save_WhenAllIsSetupCorrectly_ExpectSuccessResponse( + public async Task PostSaveMember_SaveExisting_WhenAllIsSetupCorrectly_ExpectSuccessResponse( [Frozen] IMembersUserManager umbracoMembersUserManager, - IMemberTypeService memberTypeService, - IDataTypeService dataTypeService, IMemberService memberService, - MapDefinitionCollection memberMapDefinition, - PropertyEditorCollection propertyEditorCollection, + IMemberTypeService memberTypeService, + IMemberGroupService memberGroupService, + IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IBackOfficeSecurity backOfficeSecurity) { // arrange - Member member = SetupMemberTestData(memberMapDefinition, out UmbracoMapper mapper, out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.Save); + Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.Save); Mock.Get(umbracoMembersUserManager) .Setup(x => x.FindByIdAsync(It.IsAny())) .ReturnsAsync(() => new MembersIdentityUser()); @@ -150,36 +164,36 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers .ReturnsAsync(() => IdentityResult.Success); Mock.Get(memberTypeService).Setup(x => x.GetDefault()).Returns("fakeAlias"); Mock.Get(backOfficeSecurityAccessor).Setup(x => x.BackOfficeSecurity).Returns(backOfficeSecurity); - + Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny())).Returns(() => member); Mock.Get(memberService).SetupSequence( x => x.GetByEmail(It.IsAny())) .Returns(() => null) .Returns(() => member); - MemberController sut = CreateSut(mapper, memberService, memberTypeService, umbracoMembersUserManager, dataTypeService, propertyEditorCollection, backOfficeSecurityAccessor); + MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor); // act ActionResult result = await sut.PostSave(fakeMemberData); // assert Assert.IsNull(result.Result); - Assert.AreEqual(memberDisplay, result.Value); + Assert.IsNotNull(result.Value); + AssertMemberDisplayPropertiesAreEqual(memberDisplay, result.Value); } [Test] [AutoMoqData] public void PostSaveMember_SaveNew_WhenMemberEmailAlreadyExists_ExpectFailResponse( [Frozen] IMembersUserManager umbracoMembersUserManager, - IMemberTypeService memberTypeService, - IDataTypeService dataTypeService, IMemberService memberService, - MapDefinitionCollection memberMapDefinition, - PropertyEditorCollection propertyEditorCollection, + IMemberTypeService memberTypeService, + IMemberGroupService memberGroupService, + IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IBackOfficeSecurity backOfficeSecurity) { // arrange - Member member = SetupMemberTestData(memberMapDefinition, out UmbracoMapper mapper, out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew); + Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew); Mock.Get(umbracoMembersUserManager) .Setup(x => x.CreateAsync(It.IsAny())) .ReturnsAsync(() => IdentityResult.Success); @@ -193,7 +207,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers x => x.GetByEmail(It.IsAny())) .Returns(() => member); - MemberController sut = CreateSut(mapper, memberService, memberTypeService, umbracoMembersUserManager, dataTypeService, propertyEditorCollection, backOfficeSecurityAccessor); + MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor); string reason = "Validation failed"; // act @@ -207,75 +221,130 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers } /// - /// Setup all standard member data for test + /// Create membercontroller to test /// - private Member SetupMemberTestData( - MapDefinitionCollection memberMapDefinition, - out UmbracoMapper mapper, - out MemberSave fakeMemberData, - out MemberDisplay memberDisplay, - ContentSaveAction contentAction) - { - var memberType = new MemberType(new DefaultShortStringHelper(new DefaultShortStringHelperConfig()), int.MinValue); - IMemberType testContentType = memberType; - - string fakePassword = "i2ruf38vrba8^&T^"; - var testName = "Test Name"; - var testEmail = "test@umbraco.com"; - var testUser = "TestUser"; - - var member = new Member(testName, testEmail, testUser, testContentType) { RawPasswordValue = fakePassword }; - mapper = new UmbracoMapper(memberMapDefinition); - - // TODO: reuse maps - mapper.Define((m, context) => new MemberDisplay() - { - Username = m.Username - }); - mapper.Define((m, context) => new Member(new Mock().Object)); - fakeMemberData = CreateFakeMemberData(member, contentAction); - - memberDisplay = new MemberDisplay() - { - }; - - return member; - } - + /// + /// + /// + /// + /// + /// + /// private MemberController CreateSut( - UmbracoMapper mapper, IMemberService memberService, IMemberTypeService memberTypeService, + IMemberGroupService memberGroupService, IMembersUserManager membersUserManager, IDataTypeService dataTypeService, - PropertyEditorCollection propertyEditorCollection, - IBackOfficeSecurityAccessor backOfficeSecurityAccessor) => - new MemberController( + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + var mockShortStringHelper = new MockShortStringHelper(); + + var textService = new Mock(); + var contentTypeBaseServiceProvider = new Mock(); + contentTypeBaseServiceProvider.Setup(x => x.GetContentTypeOf(It.IsAny())).Returns(new ContentType(mockShortStringHelper, 123)); + var contentAppFactories = new Mock>(); + var mockContentAppFactoryCollection = new Mock>(); + var hybridBackOfficeSecurityAccessor = new HybridBackofficeSecurityAccessor(new DictionaryAppCache()); + var contentAppFactoryCollection = new ContentAppFactoryCollection( + contentAppFactories.Object, + mockContentAppFactoryCollection.Object, + hybridBackOfficeSecurityAccessor); + var mockUserService = new Mock(); + var commonMapper = new CommonMapper( + mockUserService.Object, + contentTypeBaseServiceProvider.Object, + contentAppFactoryCollection, + textService.Object); + var mockCultureDictionary = new Mock(); + + var mockPasswordConfig = new Mock>(); + mockPasswordConfig.Setup(x => x.Value).Returns(() => new MemberPasswordConfigurationSettings()); + IDataEditor dataEditor = Mock.Of( + x => x.Type == EditorType.PropertyValue + && x.Alias == Constants.PropertyEditors.Aliases.Label); + Mock.Get(dataEditor).Setup(x => x.GetValueEditor()).Returns(new TextOnlyValueEditor(Mock.Of(), Mock.Of(), new DataEditorAttribute(Constants.PropertyEditors.Aliases.TextBox, "Test Textbox", "textbox"), textService.Object, Mock.Of(), Mock.Of())); + + var propertyEditorCollection = new PropertyEditorCollection(new DataEditorCollection(new[] { dataEditor })); + + IMapDefinition memberMapDefinition = new MemberMapDefinition( + commonMapper, + new CommonTreeNodeMapper(Mock.Of()), + new MemberTabsAndPropertiesMapper( + mockCultureDictionary.Object, + backOfficeSecurityAccessor, + textService.Object, + memberTypeService, + memberService, + memberGroupService, + mockPasswordConfig.Object, + contentTypeBaseServiceProvider.Object, + propertyEditorCollection), + new HttpContextAccessor()); + + var map = new MapDefinitionCollection(new List() + { + new global::Umbraco.Core.Models.Mapping.MemberMapDefinition(), + memberMapDefinition, + new ContentTypeMapDefinition( + commonMapper, + propertyEditorCollection, + dataTypeService, + new Mock().Object, + new Mock().Object, + new Mock().Object, + memberTypeService, + new Mock().Object, + mockShortStringHelper, + new Mock>().Object, + new Mock().Object) + }); + _mapper = new UmbracoMapper(map); + + return new MemberController( new DefaultCultureDictionary( new Mock().Object, new HttpRequestAppCache(() => null)), new LoggerFactory(), - new MockShortStringHelper(), + mockShortStringHelper, new DefaultEventMessagesFactory( new Mock().Object), - new Mock().Object, + textService.Object, propertyEditorCollection, - mapper, + _mapper, memberService, memberTypeService, membersUserManager, dataTypeService, backOfficeSecurityAccessor, new ConfigurationEditorJsonSerializer()); + } - private static MemberSave CreateFakeMemberData(IMember member, ContentSaveAction action) + + /// + /// Setup all standard member data for test + /// + private Member SetupMemberTestData( + out MemberSave fakeMemberData, + out MemberDisplay memberDisplay, + ContentSaveAction contentAction) { - var fakeMemberData = new MemberSave() + // arrange + MemberType memberType = MemberTypeBuilder.CreateSimpleMemberType(); + Member member = MemberBuilder.CreateSimpleMember(memberType, "Test Member", "test@example.com", "123", "test"); + int memberId = 123; + member.Id = memberId; + + //TODO: replace with builder for MemberSave and MemberDisplay + fakeMemberData = new MemberSave() { - Id = 123, + Id = memberId, + SortOrder = member.SortOrder, + ContentTypeId = memberType.Id, + Key = member.Key, Password = new ChangingPasswordModel() { - Id = 123, + Id = 456, NewPassword = member.RawPasswordValue, OldPassword = null }, @@ -285,13 +354,86 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers PersistedContent = member, PropertyCollectionDto = new ContentPropertyCollectionDto() { + Properties = new List() + { + new ContentPropertyDto(), + new ContentPropertyDto() + } }, Groups = new List(), - Alias = "fakeAlias", - ContentTypeAlias = "fakeContentType", - Action = action + //Alias = "fakeAlias", + ContentTypeAlias = member.ContentTypeAlias, + Action = contentAction, + Icon = "icon-document", + Path = member.Path }; - return fakeMemberData; + + memberDisplay = new MemberDisplay() + { + Id = memberId, + SortOrder = member.SortOrder, + ContentTypeId = memberType.Id, + Key = member.Key, + Name = member.Name, + Email = member.Email, + Username = member.Username, + //Alias = "fakeAlias", + ContentTypeAlias = member.ContentTypeAlias, + ContentType = new ContentTypeBasic(), + ContentTypeName = member.ContentType.Name, + Icon = fakeMemberData.Icon, + Path = member.Path, + Tabs = new List>() + { + new Tab() + { + Alias = "test" + } + } + }; + + return member; + } + + /// + /// Check all member properties are equal + /// + /// + /// + private void AssertMemberDisplayPropertiesAreEqual(MemberDisplay memberDisplay, MemberDisplay resultValue) + { + Assert.AreNotSame(memberDisplay, resultValue); + Assert.AreEqual(memberDisplay.Id, resultValue.Id); + Assert.AreEqual(memberDisplay.Alias, resultValue.Alias); + Assert.AreEqual(memberDisplay.Username, resultValue.Username); + Assert.AreEqual(memberDisplay.Email, resultValue.Email); + Assert.AreEqual(memberDisplay.AdditionalData, resultValue.AdditionalData); + Assert.AreEqual(memberDisplay.ContentApps, resultValue.ContentApps); + Assert.AreEqual(memberDisplay.ContentType.Alias, resultValue.ContentType.Alias); + Assert.AreEqual(memberDisplay.ContentTypeAlias, resultValue.ContentTypeAlias); + Assert.AreEqual(memberDisplay.ContentTypeName, resultValue.ContentTypeName); + Assert.AreEqual(memberDisplay.ContentTypeId, resultValue.ContentTypeId); + Assert.AreEqual(memberDisplay.Icon, resultValue.Icon); + Assert.AreEqual(memberDisplay.Errors, resultValue.Errors); + Assert.AreEqual(memberDisplay.Key, resultValue.Key); + Assert.AreEqual(memberDisplay.Name, resultValue.Name); + Assert.AreEqual(memberDisplay.Path, resultValue.Path); + Assert.AreEqual(memberDisplay.SortOrder, resultValue.SortOrder); + Assert.AreEqual(memberDisplay.Trashed, resultValue.Trashed); + Assert.AreEqual(memberDisplay.TreeNodeUrl, resultValue.TreeNodeUrl); + Assert.AreNotSame(memberDisplay.Properties, resultValue.Properties); + + //TODO: can we check create/update dates when saving? + //Assert.AreEqual(memberDisplay.CreateDate, resultValue.CreateDate); + //Assert.AreEqual(memberDisplay.UpdateDate, resultValue.UpdateDate); + + //TODO: check all properties + //Assert.AreEqual(memberDisplay.Properties.Count(), resultValue.Properties.Count()); + //for (var index = 0; index < resultValue.Properties.Count(); index++) + //{ + // Assert.AreNotSame(memberDisplay.Properties.GetItemByIndex(index), resultValue.Properties.GetItemByIndex(index)); + // Assert.AreEqual(memberDisplay.Properties.GetItemByIndex(index), resultValue.Properties.GetItemByIndex(index)); + //} } } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index f44909cd35..7a27e39c51 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -351,7 +351,6 @@ namespace Umbraco.Web.BackOffice.Controllers memberType.Alias, contentItem.Name); - // TODO: may not need to add password like this IdentityResult created = await _memberManager.CreateAsync(identityMember, contentItem.Password.NewPassword); if (created.Succeeded == false) @@ -362,7 +361,7 @@ namespace Umbraco.Web.BackOffice.Controllers // now re-look the member back up which will now exist IMember member = _memberService.GetByEmail(contentItem.Email); - var creatorId = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id; + int creatorId = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id; member.CreatorId = creatorId; // map the save info over onto the user @@ -476,7 +475,7 @@ namespace Umbraco.Web.BackOffice.Controllers $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password"); return false; } - } + } IMember byUsername = _memberService.GetByUsername(contentItem.Username); if (byUsername != null && byUsername.Key != contentItem.Key) From 2893dcb8b84695f0fee69600bb8989e623eb1a71 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Fri, 29 Jan 2021 11:41:58 +0000 Subject: [PATCH 41/72] Ensure last password updated date is set when password is changed during member update. Moved password logic to use identity instead of just hashing the password. --- .../Implement/MemberRepository.cs | 22 ++++++++++--------- .../Services/Implement/MemberService.cs | 5 +++-- .../Controllers/MemberControllerUnitTests.cs | 16 +++++++------- .../Controllers/MemberController.cs | 13 +++++++++-- 4 files changed, 34 insertions(+), 22 deletions(-) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs index 03f7e98b5d..58da62be1b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; @@ -336,7 +336,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } protected override void PersistUpdatedItem(IMember entity) - { + { // update entity.UpdatingEntity(); @@ -347,7 +347,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // if parent has changed, get path, level and sort order if (entity.IsPropertyDirty("ParentId")) { - var parent = GetParentNodeDto(entity.ParentId); + NodeDto parent = GetParentNodeDto(entity.ParentId); entity.Path = string.Concat(parent.Path, ",", entity.Id); entity.Level = parent.Level + 1; @@ -355,10 +355,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } // create the dto - var dto = ContentBaseFactory.BuildDto(entity); + MemberDto dto = ContentBaseFactory.BuildDto(entity); // update the node dto - var nodeDto = dto.ContentDto.NodeDto; + NodeDto nodeDto = dto.ContentDto.NodeDto; Database.Update(nodeDto); // update the content dto @@ -409,7 +409,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //get the group id var grpQry = Query().Where(group => group.Name.Equals(roleName)); var memberGroup = _memberGroupRepository.Get(grpQry).FirstOrDefault(); - if (memberGroup == null) return Enumerable.Empty(); + if (memberGroup == null) + return Enumerable.Empty(); // get the members by username var query = Query(); @@ -464,7 +465,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { var grpQry = Query().Where(group => group.Name.Equals(groupName)); var memberGroup = _memberGroupRepository.Get(grpQry).FirstOrDefault(); - if (memberGroup == null) return Enumerable.Empty(); + if (memberGroup == null) + return Enumerable.Empty(); var subQuery = Sql().Select("Member").From().Where(dto => dto.MemberGroup == memberGroup.Id); @@ -539,7 +541,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var sqlSelectTemplateVersion = SqlContext.Templates.Get("Umbraco.Core.MemberRepository.SetLastLogin2", s => s .Select(x => x.Id) - .From() + .From() .InnerJoin().On((l, r) => l.NodeId == r.NodeId) .InnerJoin().On((l, r) => l.NodeId == r.NodeId) .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType")) @@ -614,7 +616,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); if (cached != null && cached.VersionId == dto.ContentVersionDto.Id) { - content[i] = (Member) cached; + content[i] = (Member)cached; continue; } } @@ -656,7 +658,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // get properties - indexed by version id var versionId = dto.ContentVersionDto.Id; - var temp = new TempContent(dto.ContentDto.NodeId,versionId, 0, memberType); + var temp = new TempContent(dto.ContentDto.NodeId, versionId, 0, memberType); var properties = GetPropertyCollections(new List> { temp }); member.Properties = properties[versionId]; diff --git a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs index 794da8230e..721c0eee21 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs @@ -807,11 +807,11 @@ namespace Umbraco.Infrastructure.Services.Implement /// public void Save(IMember member, bool raiseEvents = true) { - //trimming username and email to make sure we have no trailing space + // trimming username and email to make sure we have no trailing space member.Username = member.Username.Trim(); member.Email = member.Email.Trim(); - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { var saveEventArgs = new SaveEventArgs(member); if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs)) @@ -834,6 +834,7 @@ namespace Umbraco.Infrastructure.Services.Implement saveEventArgs.CanCancel = false; scope.Events.Dispatch(Saved, this, saveEventArgs); } + Audit(AuditType.Save, 0, member.Id); scope.Complete(); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs index b0d4811cf6..80422dd3ed 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs @@ -221,15 +221,15 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers } /// - /// Create membercontroller to test + /// Create member controller to test /// - /// - /// - /// - /// - /// - /// - /// + /// Member service + /// Member type service + /// Member group service + /// Members user manager + /// Data type service + /// Back office security accessor + /// A member controller for the tests private MemberController CreateSut( IMemberService memberService, IMemberTypeService memberTypeService, diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index 7a27e39c51..f203fac3be 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -436,8 +436,13 @@ namespace Umbraco.Web.BackOffice.Controllers return new ValidationErrorResult(validatePassword.Errors.ToErrorMessage()); } - string newPassword = _memberManager.HashPassword(contentItem.Password.NewPassword); - identityMember.PasswordHash = newPassword; + string token = await _memberManager.GeneratePasswordResetTokenAsync(identityMember); + IdentityResult resetPassword = await _memberManager.ResetPasswordAsync(identityMember, token, contentItem.Password.NewPassword); + + if (resetPassword.Succeeded == false) + { + return new ValidationErrorResult(validatePassword.Errors.ToErrorMessage()); + } } IdentityResult updatedResult = await _memberManager.UpdateAsync(identityMember); @@ -448,6 +453,10 @@ namespace Umbraco.Web.BackOffice.Controllers } contentItem.PersistedContent.RawPasswordValue = identityMember.PasswordHash; + if (identityMember.LastPasswordChangeDateUtc != null) + { + contentItem.PersistedContent.LastPasswordChangeDate = identityMember.LastPasswordChangeDateUtc.Value; + } _memberService.Save(contentItem.PersistedContent); From f30d6f4cadfa75b61c306ad794348a5b8c4ad689 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Fri, 29 Jan 2021 13:01:59 +0000 Subject: [PATCH 42/72] Back to hashing the password instead of using the token approach --- .../Controllers/MemberController.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index f203fac3be..b2cc132ee9 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -436,13 +436,8 @@ namespace Umbraco.Web.BackOffice.Controllers return new ValidationErrorResult(validatePassword.Errors.ToErrorMessage()); } - string token = await _memberManager.GeneratePasswordResetTokenAsync(identityMember); - IdentityResult resetPassword = await _memberManager.ResetPasswordAsync(identityMember, token, contentItem.Password.NewPassword); - - if (resetPassword.Succeeded == false) - { - return new ValidationErrorResult(validatePassword.Errors.ToErrorMessage()); - } + string newPassword = _memberManager.HashPassword(contentItem.Password.NewPassword); + identityMember.PasswordHash = newPassword; } IdentityResult updatedResult = await _memberManager.UpdateAsync(identityMember); From c8d1d1625720849cf4a07d4044f1a938094c57e8 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Fri, 29 Jan 2021 13:10:02 +0000 Subject: [PATCH 43/72] Datetime.now, since identity member doesn't have lastupdated date --- src/Umbraco.Web.BackOffice/Controllers/MemberController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index b2cc132ee9..3499789083 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -450,7 +450,7 @@ namespace Umbraco.Web.BackOffice.Controllers contentItem.PersistedContent.RawPasswordValue = identityMember.PasswordHash; if (identityMember.LastPasswordChangeDateUtc != null) { - contentItem.PersistedContent.LastPasswordChangeDate = identityMember.LastPasswordChangeDateUtc.Value; + contentItem.PersistedContent.LastPasswordChangeDate = DateTime.Now; } _memberService.Save(contentItem.PersistedContent); From af07790528fd01d5cae2e5ff1d752abf79a4af47 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Fri, 29 Jan 2021 14:18:34 +0000 Subject: [PATCH 44/72] Remove comment --- src/Umbraco.Infrastructure/Security/MembersUserStore.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Umbraco.Infrastructure/Security/MembersUserStore.cs b/src/Umbraco.Infrastructure/Security/MembersUserStore.cs index b75eb19beb..517a175f88 100644 --- a/src/Umbraco.Infrastructure/Security/MembersUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MembersUserStore.cs @@ -98,8 +98,6 @@ namespace Umbraco.Infrastructure.Security // x.UserData))); //} - // TODO: confirm re roles implementations - return Task.FromResult(IdentityResult.Success); } From 7dce742b9643ca9ab7aee9cd42ab8174da447eab Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Fri, 29 Jan 2021 14:54:43 +0000 Subject: [PATCH 45/72] UTC date --- src/Umbraco.Web.BackOffice/Controllers/MemberController.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index 3499789083..24958dbde5 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -450,7 +450,8 @@ namespace Umbraco.Web.BackOffice.Controllers contentItem.PersistedContent.RawPasswordValue = identityMember.PasswordHash; if (identityMember.LastPasswordChangeDateUtc != null) { - contentItem.PersistedContent.LastPasswordChangeDate = DateTime.Now; + contentItem.PersistedContent.LastPasswordChangeDate = DateTime.UtcNow; + identityMember.LastPasswordChangeDateUtc = contentItem.PersistedContent.LastPasswordChangeDate; } _memberService.Save(contentItem.PersistedContent); From 6df67cd93e51832e425fb78966a1924ca58d0887 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Fri, 29 Jan 2021 15:03:06 +0000 Subject: [PATCH 46/72] Corrected password logic --- .../Controllers/MemberController.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index 24958dbde5..360ca9399f 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -438,6 +438,12 @@ namespace Umbraco.Web.BackOffice.Controllers string newPassword = _memberManager.HashPassword(contentItem.Password.NewPassword); identityMember.PasswordHash = newPassword; + contentItem.PersistedContent.RawPasswordValue = identityMember.PasswordHash; + if (identityMember.LastPasswordChangeDateUtc != null) + { + contentItem.PersistedContent.LastPasswordChangeDate = DateTime.UtcNow; + identityMember.LastPasswordChangeDateUtc = contentItem.PersistedContent.LastPasswordChangeDate; + } } IdentityResult updatedResult = await _memberManager.UpdateAsync(identityMember); @@ -447,13 +453,6 @@ namespace Umbraco.Web.BackOffice.Controllers return new ValidationErrorResult(updatedResult.Errors.ToErrorMessage()); } - contentItem.PersistedContent.RawPasswordValue = identityMember.PasswordHash; - if (identityMember.LastPasswordChangeDateUtc != null) - { - contentItem.PersistedContent.LastPasswordChangeDate = DateTime.UtcNow; - identityMember.LastPasswordChangeDateUtc = contentItem.PersistedContent.LastPasswordChangeDate; - } - _memberService.Save(contentItem.PersistedContent); AddOrUpdateRoles(contentItem); From 8caf2a0e6238b836e72afb4862ad0f88f8b5a9b9 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Fri, 29 Jan 2021 16:43:50 +0000 Subject: [PATCH 47/72] Initial check in of roles work in the store. Not currently functional --- .../Security/BackOfficeUserStore.cs | 2 +- .../Security/IUmbracoUserManager.cs | 25 +++- .../Security/MembersIdentityUser.cs | 4 +- .../Security/MembersUserStore.cs | 126 +++++++++++++++--- .../Security/UmbracoUserManager.cs | 7 +- .../Controllers/MemberController.cs | 23 ++-- .../Security/BackOfficeUserManager.cs | 2 +- .../Security/MembersUserManager.cs | 2 + 8 files changed, 152 insertions(+), 39 deletions(-) diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs index 161be87677..a36282ffb1 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs @@ -438,7 +438,7 @@ namespace Umbraco.Infrastructure.Security } /// - /// Returns the roles (user groups) for this user + /// Gets a list of role names the specified user belongs to. /// public override Task> GetRolesAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default) { diff --git a/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs b/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs index fa3d7a691a..9e44855a4a 100644 --- a/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Umbraco.Core.Models.Identity; using Umbraco.Core.Models.Membership; -using Umbraco.Core.Security; using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Infrastructure.Security @@ -224,6 +223,28 @@ namespace Umbraco.Infrastructure.Security /// Task CreateAsync(TUser user); + /// + /// Gets a list of role names the specified user belongs to. + /// + /// The user whose role names to retrieve. + /// The Task that represents the asynchronous operation, containing a list of role names. + Task> GetRolesAsync(TUser user); + + /// + /// Removes the specified user from the named roles. + /// + /// The user to remove from the named roles. + /// The name of the roles to remove the user from. + /// The Task that represents the asynchronous operation, containing the IdentityResult of the operation. + Task RemoveFromRolesAsync(TUser user, IEnumerable roles); + + /// + /// Add the specified user to the named roles + /// + /// The user to add to the named roles + /// The name of the roles to add the user to. + /// The Task that represents the asynchronous operation, containing the IdentityResult of the operation + Task AddToRolesAsync(TUser user, IEnumerable roles); /// /// Creates the specified in the backing store with a password, @@ -236,7 +257,7 @@ namespace Umbraco.Infrastructure.Security /// of the operation. /// Task CreateAsync(TUser user, string password); - + /// /// Generate a password for a user based on the current password validator /// diff --git a/src/Umbraco.Infrastructure/Security/MembersIdentityUser.cs b/src/Umbraco.Infrastructure/Security/MembersIdentityUser.cs index 66ed733315..c3376279b7 100644 --- a/src/Umbraco.Infrastructure/Security/MembersIdentityUser.cs +++ b/src/Umbraco.Infrastructure/Security/MembersIdentityUser.cs @@ -56,7 +56,7 @@ namespace Umbraco.Infrastructure.Security user.EnableChangeTracking(); return user; } - + /// /// Gets or sets the member's real name /// @@ -89,7 +89,7 @@ namespace Umbraco.Infrastructure.Security foreach (IdentityUserRole identityUserRole in _groups.Select(x => new IdentityUserRole { RoleId = x.Alias, - UserId = Id?.ToString() + UserId = Id })) { roles.Add(identityUserRole); diff --git a/src/Umbraco.Infrastructure/Security/MembersUserStore.cs b/src/Umbraco.Infrastructure/Security/MembersUserStore.cs index 517a175f88..ebcd340d7c 100644 --- a/src/Umbraco.Infrastructure/Security/MembersUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MembersUserStore.cs @@ -303,7 +303,7 @@ namespace Umbraco.Infrastructure.Security return Task.FromResult((IList)user.Logins.Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey, l.LoginProvider)).ToList()); } - + /// protected override async Task> FindUserLoginAsync(string userId, string loginProvider, string providerKey, CancellationToken cancellationToken) { @@ -355,10 +355,8 @@ namespace Umbraco.Infrastructure.Security }); } - /// - /// Adds a user to a role (user group) - /// - public override Task AddToRoleAsync(MembersIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default) + /// + public override Task AddToRoleAsync(MembersIdentityUser user, string role, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -367,30 +365,76 @@ namespace Umbraco.Infrastructure.Security throw new ArgumentNullException(nameof(user)); } - if (normalizedRoleName == null) + if (role == null) { - throw new ArgumentNullException(nameof(normalizedRoleName)); + throw new ArgumentNullException(nameof(role)); } - if (string.IsNullOrWhiteSpace(normalizedRoleName)) + if (string.IsNullOrWhiteSpace(role)) { - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(normalizedRoleName)); + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(role)); } - IdentityUserRole userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName); + IdentityUserRole userRole = user.Roles.SingleOrDefault(r => r.RoleId == role); if (userRole == null) { - user.AddRole(normalizedRoleName); + user.AddRole(role); } return Task.CompletedTask; } /// - /// Removes the role (user group) for the user + /// Add the specified user to the named roles /// - public override Task RemoveFromRoleAsync(MembersIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default) + /// The user to add to the named roles + /// The name of the roles to add the user to. + /// The cancellation token + /// The Task that represents the asynchronous operation, containing the IdentityResult of the operation + public Task AddToRolesAsync(MembersIdentityUser user, IEnumerable roles, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (roles == null) + { + throw new ArgumentNullException(nameof(roles)); + } + + IEnumerable enumerable = roles as string[] ?? roles.ToArray(); + foreach (string role in enumerable) + { + if (string.IsNullOrWhiteSpace(role)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(role)); + } + + IdentityUserRole userRole = user.Roles.SingleOrDefault(r => r.RoleId == role); + + if (userRole == null) + { + user.AddRole(role); + } + } + + _memberService.AssignRoles(new[] { user.UserName }, enumerable.ToArray()); + + return Task.CompletedTask; + } + + /// + /// Removes the specified user from the named roles. + /// + /// The user to remove from the named roles. + /// The name of the roles to remove the user from. + /// The cancellation token + /// The Task that represents the asynchronous operation, containing the IdentityResult of the operation. + public Task RemoveFromRolesAsync(MembersIdentityUser user, IEnumerable roles, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); @@ -399,20 +443,62 @@ namespace Umbraco.Infrastructure.Security throw new ArgumentNullException(nameof(user)); } - if (normalizedRoleName == null) + if (roles == null) { - throw new ArgumentNullException(nameof(normalizedRoleName)); + throw new ArgumentNullException(nameof(roles)); } - if (string.IsNullOrWhiteSpace(normalizedRoleName)) + IEnumerable enumerable = roles as string[] ?? roles.ToArray(); + foreach (string role in enumerable) { - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(normalizedRoleName)); + if (string.IsNullOrWhiteSpace(role)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(role)); + } + + //TODO: is the role ID the role string passed in? + IdentityUserRole userRole = user.Roles.SingleOrDefault(r => r.RoleId == role); + + if (userRole != null) + { + user.Roles.Remove(userRole); + } } - IdentityUserRole userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName); + //TODO: confirm that when updating the identity member, we're also calling the service to update in the DB via repository + _memberService.DissociateRoles(new[] { user.UserName }, enumerable.ToArray()); + + return Task.CompletedTask; + } + + //TODO: should we call the single remove from the multiple remove? or have it only in one place? + /// + public override Task RemoveFromRoleAsync(MembersIdentityUser user, string role, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + + if (string.IsNullOrWhiteSpace(role)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(role)); + } + + //TODO: is the role ID the role string passed in? + IdentityUserRole userRole = user.Roles.SingleOrDefault(r => r.RoleId == role); if (userRole != null) { + //TODO: when updating the identity member, we're also calling the service to update in the DB via repository + _memberService.DissociateRole(userRole.UserId, userRole.RoleId); user.Roles.Remove(userRole); } @@ -420,7 +506,7 @@ namespace Umbraco.Infrastructure.Security } /// - /// Returns the roles (user groups) for this user + /// Gets a list of role names the specified user belongs to. /// public override Task> GetRolesAsync(MembersIdentityUser user, CancellationToken cancellationToken = default) { @@ -431,6 +517,8 @@ namespace Umbraco.Infrastructure.Security throw new ArgumentNullException(nameof(user)); } + //TODO: should we have tests for the store? + IEnumerable currentRoles = _memberService.GetAllRoles(user.UserName); return Task.FromResult((IList)user.Roles.Select(x => x.RoleId).ToList()); } diff --git a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs index b14cada788..7c767865b0 100644 --- a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs @@ -102,12 +102,9 @@ namespace Umbraco.Infrastructure.Security /// The generated password public string GeneratePassword() { - if (_passwordGenerator == null) - { - _passwordGenerator = new PasswordGenerator(PasswordConfiguration); - } + _passwordGenerator ??= new PasswordGenerator(PasswordConfiguration); - var password = _passwordGenerator.GeneratePassword(); + string password = _passwordGenerator.GeneratePassword(); return password; } diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index 3499789083..8208ed9789 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -371,7 +371,7 @@ namespace Umbraco.Web.BackOffice.Controllers _memberService.Save(member); contentItem.PersistedContent = member; - AddOrUpdateRoles(contentItem); + AddOrUpdateRoles(contentItem, identityMember); return true; } @@ -455,7 +455,7 @@ namespace Umbraco.Web.BackOffice.Controllers _memberService.Save(contentItem.PersistedContent); - AddOrUpdateRoles(contentItem); + AddOrUpdateRoles(contentItem, identityMember); return true; } @@ -479,7 +479,7 @@ namespace Umbraco.Web.BackOffice.Controllers $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password"); return false; } - } + } IMember byUsername = _memberService.GetByUsername(contentItem.Username); if (byUsername != null && byUsername.Key != contentItem.Key) @@ -516,16 +516,18 @@ namespace Umbraco.Web.BackOffice.Controllers } /// - /// TODO: refactor using identity roles + /// Add or update the identity roles /// - /// - private void AddOrUpdateRoles(MemberSave contentItem) + /// The member content item + /// The member as an identity user + private async Task AddOrUpdateRoles(MemberSave contentItem, MembersIdentityUser identityMember) { // We're gonna look up the current roles now because the below code can cause // events to be raised and developers could be manually adding roles to members in // their handlers. If we don't look this up now there's a chance we'll just end up // removing the roles they've assigned. - IEnumerable currentRoles = _memberService.GetAllRoles(contentItem.PersistedContent.Username); + IEnumerable currentRoles = await _memberManager.GetRolesAsync(identityMember); + //IEnumerable currentRoles = _memberService.GetAllRoles(contentItem.PersistedContent.Username); // find the ones to remove and remove them IEnumerable roles = currentRoles.ToList(); @@ -535,7 +537,8 @@ namespace Umbraco.Web.BackOffice.Controllers // if we are changing the username, it must be persisted before looking up the member roles). if (rolesToRemove.Any()) { - _memberService.DissociateRoles(new[] { contentItem.PersistedContent.Username }, rolesToRemove); + await _memberManager.RemoveFromRolesAsync(identityMember, rolesToRemove); + //_memberService.DissociateRoles(new[] { contentItem.PersistedContent.Username }, rolesToRemove); } // find the ones to add and add them @@ -543,7 +546,8 @@ namespace Umbraco.Web.BackOffice.Controllers if (toAdd.Any()) { // add the ones submitted - _memberService.AssignRoles(new[] { contentItem.PersistedContent.Username }, toAdd); + IdentityResult identityResult = await _memberManager.AddToRolesAsync(identityMember, toAdd); + //_memberService.AssignRoles(new[] { contentItem.PersistedContent.Username }, toAdd); } } @@ -556,6 +560,7 @@ namespace Umbraco.Web.BackOffice.Controllers [HttpPost] public IActionResult DeleteByKey(Guid key) { + //TODO: move to MembersUserStore IMember foundMember = _memberService.GetByKey(key); if (foundMember == null) { diff --git a/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs index ca1a3f561d..b50fd6243a 100644 --- a/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs @@ -138,7 +138,7 @@ namespace Umbraco.Web.Common.Security return result; } - + /// public override async Task SetLockoutEndDateAsync(BackOfficeIdentityUser user, DateTimeOffset? lockoutEnd) { diff --git a/src/Umbraco.Web.Common/Security/MembersUserManager.cs b/src/Umbraco.Web.Common/Security/MembersUserManager.cs index 1604c056b0..f1d6c963a9 100644 --- a/src/Umbraco.Web.Common/Security/MembersUserManager.cs +++ b/src/Umbraco.Web.Common/Security/MembersUserManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Security.Principal; +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; @@ -53,6 +54,7 @@ namespace Umbraco.Web.Common.Security } //TODO: have removed all other member audit events - can revisit if we need member auditing on a user level in future + public void RaiseForgotPasswordRequestedEvent(IPrincipal currentUser, string userId) => throw new NotImplementedException(); public void RaiseForgotPasswordChangedSuccessEvent(IPrincipal currentUser, string userId) => throw new NotImplementedException(); From 152ad9684c255f58d2678c768ee6564addd5f370 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Mon, 1 Feb 2021 17:43:11 +0000 Subject: [PATCH 48/72] Username passed into roles. Added initial roles store functionality. Updated user roles functionality to persist the member group. --- .../Security/MemberRolesUserStore.cs | 57 +++++++++ .../Security/MembersUserStore.cs | 111 +++--------------- .../AutoFixture/AutoMoqDataAttribute.cs | 3 +- .../Controllers/MemberController.cs | 2 - .../Security/MembersUserManager.cs | 1 - src/Umbraco.Web/Security/MembershipHelper.cs | 51 +++++--- .../Security/MembershipProviderBase.cs | 9 +- .../Providers/MembersMembershipProvider.cs | 6 +- .../Security/Providers/MembersRoleProvider.cs | 3 + .../Providers/UmbracoMembershipProvider.cs | 23 ++-- 10 files changed, 133 insertions(+), 133 deletions(-) create mode 100644 src/Umbraco.Infrastructure/Security/MemberRolesUserStore.cs diff --git a/src/Umbraco.Infrastructure/Security/MemberRolesUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberRolesUserStore.cs new file mode 100644 index 0000000000..771005b09b --- /dev/null +++ b/src/Umbraco.Infrastructure/Security/MemberRolesUserStore.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Umbraco.Core.Scoping; +using Umbraco.Core.Services; + +namespace Umbraco.Infrastructure.Security +{ + /// + /// A custom user store that uses Umbraco member data + /// + public class MemberRolesUserStore : RoleStoreBase, string, IdentityUserRole, IdentityRoleClaim> + { + private readonly IMemberService _memberService; + private readonly IMemberGroupService _memberGroupService; + private readonly IScopeProvider _scopeProvider; + + public MemberRolesUserStore(IMemberService memberService, IMemberGroupService memberGroupService, IScopeProvider scopeProvider, IdentityErrorDescriber describer) + : base(describer) + { + _memberService = memberService ?? throw new ArgumentNullException(nameof(memberService)); + _memberGroupService = memberGroupService ?? throw new ArgumentNullException(nameof(memberGroupService)); + _scopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider)); + } + + /// + public override IQueryable> Roles { get; } + + /// + public override Task CreateAsync(IdentityRole role, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException(); + + /// + public override Task UpdateAsync(IdentityRole role, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException(); + + /// + public override Task DeleteAsync(IdentityRole role, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException(); + + /// + public override Task> FindByIdAsync(string id, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException(); + + /// + public override Task> FindByNameAsync(string normalizedName, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException(); + + /// + public override Task> GetClaimsAsync(IdentityRole role, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException(); + + /// + public override Task AddClaimAsync(IdentityRole role, Claim claim, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException(); + + /// + public override Task RemoveClaimAsync(IdentityRole role, Claim claim, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException(); + } +} diff --git a/src/Umbraco.Infrastructure/Security/MembersUserStore.cs b/src/Umbraco.Infrastructure/Security/MembersUserStore.cs index ebcd340d7c..d7defd8da5 100644 --- a/src/Umbraco.Infrastructure/Security/MembersUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MembersUserStore.cs @@ -32,13 +32,12 @@ namespace Umbraco.Infrastructure.Security /// The mapper for properties /// The scope provider /// The error describer - /// public MembersUserStore(IMemberService memberService, UmbracoMapper mapper, IScopeProvider scopeProvider, IdentityErrorDescriber describer) : base(describer) { _memberService = memberService ?? throw new ArgumentNullException(nameof(memberService)); - _mapper = mapper; - _scopeProvider = scopeProvider; + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + _scopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider)); } /// @@ -55,7 +54,7 @@ namespace Umbraco.Infrastructure.Security public override Task SetNormalizedUserNameAsync(MembersIdentityUser user, string normalizedName, CancellationToken cancellationToken) => SetUserNameAsync(user, normalizedName, cancellationToken); /// - public override Task CreateAsync(MembersIdentityUser user, CancellationToken cancellationToken) + public override Task CreateAsync(MembersIdentityUser user, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -338,8 +337,10 @@ namespace Umbraco.Infrastructure.Security cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); + var logins = new List(); + // TODO: external login needed? - var logins = new List(); //_externalLoginService.Find(loginProvider, providerKey).ToList(); + //_externalLoginService.Find(loginProvider, providerKey).ToList(); if (logins.Count == 0) { return Task.FromResult((IdentityUserLogin)null); @@ -379,99 +380,13 @@ namespace Umbraco.Infrastructure.Security if (userRole == null) { + _memberService.AssignRole(user.UserName, role); user.AddRole(role); } return Task.CompletedTask; } - /// - /// Add the specified user to the named roles - /// - /// The user to add to the named roles - /// The name of the roles to add the user to. - /// The cancellation token - /// The Task that represents the asynchronous operation, containing the IdentityResult of the operation - public Task AddToRolesAsync(MembersIdentityUser user, IEnumerable roles, CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - if (roles == null) - { - throw new ArgumentNullException(nameof(roles)); - } - - IEnumerable enumerable = roles as string[] ?? roles.ToArray(); - foreach (string role in enumerable) - { - if (string.IsNullOrWhiteSpace(role)) - { - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(role)); - } - - IdentityUserRole userRole = user.Roles.SingleOrDefault(r => r.RoleId == role); - - if (userRole == null) - { - user.AddRole(role); - } - } - - _memberService.AssignRoles(new[] { user.UserName }, enumerable.ToArray()); - - return Task.CompletedTask; - } - - /// - /// Removes the specified user from the named roles. - /// - /// The user to remove from the named roles. - /// The name of the roles to remove the user from. - /// The cancellation token - /// The Task that represents the asynchronous operation, containing the IdentityResult of the operation. - public Task RemoveFromRolesAsync(MembersIdentityUser user, IEnumerable roles, CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - if (roles == null) - { - throw new ArgumentNullException(nameof(roles)); - } - - IEnumerable enumerable = roles as string[] ?? roles.ToArray(); - foreach (string role in enumerable) - { - if (string.IsNullOrWhiteSpace(role)) - { - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(role)); - } - - //TODO: is the role ID the role string passed in? - IdentityUserRole userRole = user.Roles.SingleOrDefault(r => r.RoleId == role); - - if (userRole != null) - { - user.Roles.Remove(userRole); - } - } - - //TODO: confirm that when updating the identity member, we're also calling the service to update in the DB via repository - _memberService.DissociateRoles(new[] { user.UserName }, enumerable.ToArray()); - - return Task.CompletedTask; - } - - //TODO: should we call the single remove from the multiple remove? or have it only in one place? /// public override Task RemoveFromRoleAsync(MembersIdentityUser user, string role, CancellationToken cancellationToken = default) { @@ -492,13 +407,11 @@ namespace Umbraco.Infrastructure.Security throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(role)); } - //TODO: is the role ID the role string passed in? IdentityUserRole userRole = user.Roles.SingleOrDefault(r => r.RoleId == role); if (userRole != null) { - //TODO: when updating the identity member, we're also calling the service to update in the DB via repository - _memberService.DissociateRole(userRole.UserId, userRole.RoleId); + _memberService.DissociateRole(user.UserName, userRole.RoleId); user.Roles.Remove(userRole); } @@ -517,8 +430,14 @@ namespace Umbraco.Infrastructure.Security throw new ArgumentNullException(nameof(user)); } - //TODO: should we have tests for the store? IEnumerable currentRoles = _memberService.GetAllRoles(user.UserName); + ICollection> roles = currentRoles.Select(role => new IdentityUserRole + { + RoleId = role, + UserId = user.Id + }).ToList(); + + user.Roles = roles; return Task.FromResult((IList)user.Roles.Select(x => x.RoleId).ToList()); } diff --git a/src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs b/src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs index 8097cbed92..ca95c73345 100644 --- a/src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs +++ b/src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs @@ -54,7 +54,8 @@ namespace Umbraco.Tests.UnitTests.AutoFixture .Customize(new ConstructorCustomization(typeof(PreviewController), new GreedyConstructorQuery())) .Customize(new ConstructorCustomization(typeof(MemberController), new GreedyConstructorQuery())) .Customize(new ConstructorCustomization(typeof(BackOfficeController), new GreedyConstructorQuery())) - .Customize(new ConstructorCustomization(typeof(BackOfficeUserManager), new GreedyConstructorQuery())); + .Customize(new ConstructorCustomization(typeof(BackOfficeUserManager), new GreedyConstructorQuery())) + .Customize(new ConstructorCustomization(typeof(MembersUserManager), new GreedyConstructorQuery())); fixture.Customize(new AutoMoqCustomization()); diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index 8208ed9789..25f27f571f 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -527,7 +527,6 @@ namespace Umbraco.Web.BackOffice.Controllers // their handlers. If we don't look this up now there's a chance we'll just end up // removing the roles they've assigned. IEnumerable currentRoles = await _memberManager.GetRolesAsync(identityMember); - //IEnumerable currentRoles = _memberService.GetAllRoles(contentItem.PersistedContent.Username); // find the ones to remove and remove them IEnumerable roles = currentRoles.ToList(); @@ -538,7 +537,6 @@ namespace Umbraco.Web.BackOffice.Controllers if (rolesToRemove.Any()) { await _memberManager.RemoveFromRolesAsync(identityMember, rolesToRemove); - //_memberService.DissociateRoles(new[] { contentItem.PersistedContent.Username }, rolesToRemove); } // find the ones to add and add them diff --git a/src/Umbraco.Web.Common/Security/MembersUserManager.cs b/src/Umbraco.Web.Common/Security/MembersUserManager.cs index f1d6c963a9..51206bce61 100644 --- a/src/Umbraco.Web.Common/Security/MembersUserManager.cs +++ b/src/Umbraco.Web.Common/Security/MembersUserManager.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Security.Principal; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; diff --git a/src/Umbraco.Web/Security/MembershipHelper.cs b/src/Umbraco.Web/Security/MembershipHelper.cs index 5cee61834e..639b9c396c 100644 --- a/src/Umbraco.Web/Security/MembershipHelper.cs +++ b/src/Umbraco.Web/Security/MembershipHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -11,7 +11,6 @@ using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Models.Security; using Umbraco.Core.Services; using Umbraco.Core.Strings; -using Umbraco.Web.Editors; using Umbraco.Web.Models; using Umbraco.Web.PublishedCache; using Umbraco.Web.Security.Providers; @@ -19,6 +18,15 @@ using Umbraco.Web.Security.Providers; namespace Umbraco.Web.Security { // MIGRATED TO NETCORE + // TODO: Analyse all - much can be moved/removed since most methods will occur on the manager via identity implementation + + /// + /// Helper class containing logic relating to the built-in Umbraco members macros and controllers for: + /// - Registration + /// - Updating + /// - Logging in + /// - Current status + /// public class MembershipHelper { private readonly MembersMembershipProvider _membershipProvider; @@ -118,7 +126,7 @@ namespace Umbraco.Web.Security var pathsWithAccess = HasAccess(pathsWithProtection, Roles.Provider); var result = new Dictionary(); - foreach(var path in paths) + foreach (var path in paths) { pathsWithAccess.TryGetValue(path, out var hasAccess); // if it's not found it's false anyways @@ -144,7 +152,8 @@ namespace Umbraco.Web.Security string[] userRoles = null; string[] getUserRoles(string username) { - if (userRoles != null) return userRoles; + if (userRoles != null) + return userRoles; userRoles = roleProvider.GetRolesForUser(username).ToArray(); return userRoles; } @@ -185,7 +194,8 @@ namespace Umbraco.Web.Security var provider = _membershipProvider; var membershipUser = provider.GetCurrentUser(); //NOTE: This should never happen since they are logged in - if (membershipUser == null) throw new InvalidOperationException("Could not find member with username " + _httpContextAccessor.GetRequiredHttpContext().User.Identity.Name); + if (membershipUser == null) + throw new InvalidOperationException("Could not find member with username " + _httpContextAccessor.GetRequiredHttpContext().User.Identity.Name); try { @@ -257,7 +267,8 @@ namespace Umbraco.Web.Security null, null, true, null, out status); - if (status != MembershipCreateStatus.Success) return null; + if (status != MembershipCreateStatus.Success) + return null; var member = _memberService.GetByUsername(membershipUser.UserName); member.Name = model.Name; @@ -367,7 +378,8 @@ namespace Umbraco.Web.Security public virtual IPublishedContent Get(Udi udi) { var guidUdi = udi as GuidUdi; - if (guidUdi == null) return null; + if (guidUdi == null) + return null; var umbracoType = UdiEntityTypeHelper.ToUmbracoObjectType(udi.EntityType); @@ -702,27 +714,32 @@ namespace Umbraco.Web.Security if (email != null) { - if (member.Email != email) update = true; + if (member.Email != email) + update = true; member.Email = email; } if (isApproved.HasValue) { - if (member.IsApproved != isApproved.Value) update = true; + if (member.IsApproved != isApproved.Value) + update = true; member.IsApproved = isApproved.Value; } if (lastLoginDate.HasValue) { - if (member.LastLoginDate != lastLoginDate.Value) update = true; + if (member.LastLoginDate != lastLoginDate.Value) + update = true; member.LastLoginDate = lastLoginDate.Value; } if (lastActivityDate.HasValue) { - if (member.LastActivityDate != lastActivityDate.Value) update = true; + if (member.LastActivityDate != lastActivityDate.Value) + update = true; member.LastActivityDate = lastActivityDate.Value; } if (comment != null) { - if (member.Comment != comment) update = true; + if (member.Comment != comment) + update = true; member.Comment = comment; } @@ -741,8 +758,8 @@ namespace Umbraco.Web.Security { var provider = _membershipProvider; - var username = provider.GetCurrentUserName(); - // The result of this is cached by the MemberRepository + var username = provider.GetCurrentUserName(); + // The result of this is cached by the MemberRepository var member = _memberService.GetByUsername(username); return member; } @@ -763,8 +780,10 @@ namespace Umbraco.Web.Security // YES! It is completely insane how many options you have to take into account based on the membership provider. yikes! - if (passwordModel == null) throw new ArgumentNullException(nameof(passwordModel)); - if (membershipProvider == null) throw new ArgumentNullException(nameof(membershipProvider)); + if (passwordModel == null) + throw new ArgumentNullException(nameof(passwordModel)); + if (membershipProvider == null) + throw new ArgumentNullException(nameof(membershipProvider)); var userId = -1; diff --git a/src/Umbraco.Web/Security/MembershipProviderBase.cs b/src/Umbraco.Web/Security/MembershipProviderBase.cs index 669b105775..29f694803b 100644 --- a/src/Umbraco.Web/Security/MembershipProviderBase.cs +++ b/src/Umbraco.Web/Security/MembershipProviderBase.cs @@ -1,25 +1,22 @@ -using System; +using System; using System.Collections.Specialized; using System.ComponentModel.DataAnnotations; using System.Configuration.Provider; using System.Text; using System.Text.RegularExpressions; -using System.Web; -using System.Web.Hosting; -using System.Web.Configuration; using System.Web.Security; using Microsoft.Extensions.Logging; using Umbraco.Core; using Umbraco.Web.Composing; using Umbraco.Core.Hosting; -using Umbraco.Core.Security; namespace Umbraco.Web.Security { + //TODO: Delete - should not be used /// /// A base membership provider class offering much of the underlying functionality for initializing and password encryption/hashing. /// - [Obsolete("Will be replaced by UmbracoMemberUserManager")] + [Obsolete("We are now using ASP.NET Core Identity instead of membership providers")] public abstract class MembershipProviderBase : MembershipProvider { private readonly IHostingEnvironment _hostingEnvironment; diff --git a/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs b/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs index fa1ddae980..92ea0c42f0 100644 --- a/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs +++ b/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs @@ -1,4 +1,4 @@ -using System.Collections.Specialized; +using System.Collections.Specialized; using System.Configuration.Provider; using System.Web.Security; using Umbraco.Core; @@ -7,13 +7,14 @@ using Umbraco.Core.Hosting; using Umbraco.Core.Models; using Umbraco.Core.Security; using Umbraco.Core.Services; -using Umbraco.Core.Models.Membership; using Umbraco.Web.Composing; using System; using Umbraco.Net; namespace Umbraco.Web.Security.Providers { + //TODO: Delete: should not be used + [Obsolete("We are now using ASP.NET Core Identity instead of membership providers")] /// /// Custom Membership Provider for Umbraco Members (User authentication for Frontend applications NOT umbraco CMS) /// @@ -120,6 +121,7 @@ namespace Umbraco.Web.Security.Providers public override LegacyPasswordSecurity PasswordSecurity => _passwordSecurity.Value; public IPasswordConfiguration PasswordConfiguration => _passwordConfig.Value; + [Obsolete("We are now using ASP.NET Core Identity instead of membership providers")] private class MembershipProviderPasswordConfiguration : IPasswordConfiguration { public MembershipProviderPasswordConfiguration(int requiredLength, bool requireNonLetterOrDigit, bool requireDigit, bool requireLowercase, bool requireUppercase, bool useLegacyEncoding, string hashAlgorithmType, int maxFailedAccessAttemptsBeforeLockout) diff --git a/src/Umbraco.Web/Security/Providers/MembersRoleProvider.cs b/src/Umbraco.Web/Security/Providers/MembersRoleProvider.cs index 0637b08621..b1b4657d3e 100644 --- a/src/Umbraco.Web/Security/Providers/MembersRoleProvider.cs +++ b/src/Umbraco.Web/Security/Providers/MembersRoleProvider.cs @@ -1,3 +1,4 @@ +using System; using System.Configuration.Provider; using System.Linq; using System.Web.Security; @@ -8,6 +9,8 @@ using Umbraco.Web.Composing; namespace Umbraco.Web.Security.Providers { + //TODO: Delete: should not be used + [Obsolete("We are now using ASP.NET Core Identity instead of membership providers")] public class MembersRoleProvider : RoleProvider { private readonly IMembershipRoleService _roleService; diff --git a/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs b/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs index 8a92226d7e..51f31b577b 100644 --- a/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs +++ b/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Specialized; using System.Configuration.Provider; using System.Linq; @@ -17,8 +17,8 @@ using Umbraco.Web.Composing; namespace Umbraco.Web.Security.Providers { - - + //TODO: Delete - should not be used + [Obsolete("We are now using ASP.NET Core Identity instead of membership providers")] /// /// Abstract Membership Provider that users any implementation of IMembershipMemberService{TEntity} service /// @@ -32,7 +32,7 @@ namespace Umbraco.Web.Security.Providers protected IMembershipMemberService MemberService { get; private set; } protected UmbracoMembershipProvider(IMembershipMemberService memberService, IUmbracoVersion umbracoVersion, IHostingEnvironment hostingEnvironment, IIpResolver ipResolver) - :base(hostingEnvironment) + : base(hostingEnvironment) { _umbracoVersion = umbracoVersion; _ipResolver = ipResolver; @@ -55,9 +55,11 @@ namespace Umbraco.Web.Security.Providers /// The name of the provider has a length of zero. public override void Initialize(string name, NameValueCollection config) { - if (config == null) { throw new ArgumentNullException("config"); } + if (config == null) + { throw new ArgumentNullException("config"); } - if (string.IsNullOrEmpty(name)) name = ProviderName; + if (string.IsNullOrEmpty(name)) + name = ProviderName; // Initialize base provider class base.Initialize(name, config); @@ -80,7 +82,8 @@ namespace Umbraco.Web.Security.Providers // in order to support updating passwords from the umbraco core, we can't validate the old password var m = MemberService.GetByUsername(username); - if (m == null) return false; + if (m == null) + return false; string salt; var encodedPassword = PasswordSecurity.HashNewPassword(Membership.HashAlgorithmType, newPassword, out salt); @@ -174,7 +177,8 @@ namespace Umbraco.Web.Security.Providers public override bool DeleteUser(string username, bool deleteAllRelatedData) { var member = MemberService.GetByUsername(username); - if (member == null) return false; + if (member == null) + return false; MemberService.Delete(member); return true; @@ -423,7 +427,8 @@ namespace Umbraco.Web.Security.Providers } // Non need to update - if (member.IsLockedOut == false) return true; + if (member.IsLockedOut == false) + return true; member.IsLockedOut = false; member.FailedPasswordAttempts = 0; From ecf3650f53d8bf375c493be7fcef9f45b6f31459 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Mon, 1 Feb 2021 18:11:13 +0000 Subject: [PATCH 49/72] Remove comment --- src/Umbraco.Web.BackOffice/Controllers/MemberController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index befaf6acea..e2117962be 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -545,7 +545,6 @@ namespace Umbraco.Web.BackOffice.Controllers { // add the ones submitted IdentityResult identityResult = await _memberManager.AddToRolesAsync(identityMember, toAdd); - //_memberService.AssignRoles(new[] { contentItem.PersistedContent.Username }, toAdd); } } From 5053c97e028c7163093e9aeaa894d5bf1d383890 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Mon, 1 Feb 2021 18:13:23 +0000 Subject: [PATCH 50/72] Await expressions --- src/Umbraco.Web.BackOffice/Controllers/MemberController.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index e2117962be..5e96c5936f 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -371,7 +371,7 @@ namespace Umbraco.Web.BackOffice.Controllers _memberService.Save(member); contentItem.PersistedContent = member; - AddOrUpdateRoles(contentItem, identityMember); + await AddOrUpdateRoles(contentItem, identityMember); return true; } @@ -455,7 +455,7 @@ namespace Umbraco.Web.BackOffice.Controllers _memberService.Save(contentItem.PersistedContent); - AddOrUpdateRoles(contentItem, identityMember); + await AddOrUpdateRoles(contentItem, identityMember); return true; } @@ -536,7 +536,7 @@ namespace Umbraco.Web.BackOffice.Controllers // if we are changing the username, it must be persisted before looking up the member roles). if (rolesToRemove.Any()) { - await _memberManager.RemoveFromRolesAsync(identityMember, rolesToRemove); + IdentityResult rolesIdentityResult = await _memberManager.RemoveFromRolesAsync(identityMember, rolesToRemove); } // find the ones to add and add them From 512361f504edbfddfb258f73020586f14f7baf41 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Wed, 3 Feb 2021 17:33:48 +0000 Subject: [PATCH 51/72] Added controller role (member group) tests. Fix for custom member properties not saving. Updated custom member type saving. --- .../Security/MembersUserStore.cs | 1 - .../Controllers/MemberControllerUnitTests.cs | 116 ++++++++++++++++-- .../Controllers/MemberController.cs | 21 +++- 3 files changed, 126 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Infrastructure/Security/MembersUserStore.cs b/src/Umbraco.Infrastructure/Security/MembersUserStore.cs index d7defd8da5..744919cfa1 100644 --- a/src/Umbraco.Infrastructure/Security/MembersUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MembersUserStore.cs @@ -70,7 +70,6 @@ namespace Umbraco.Infrastructure.Security user.Name.IsNullOrWhiteSpace() ? user.UserName : user.Name, user.MemberTypeAlias.IsNullOrWhiteSpace() ? Constants.Security.DefaultMemberTypeAlias : user.MemberTypeAlias); - UpdateMemberProperties(memberEntity, user); // create the member diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs index 80422dd3ed..2d7e035920 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs @@ -96,10 +96,9 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers Assert.AreEqual(StatusCodes.Status400BadRequest, validation?.StatusCode); } - [Test] [AutoMoqData] - public async Task PostSaveMember_SaveNew_WhenAllIsSetupCorrectly_ExpectSuccessResponse( + public async Task PostSaveMember_SaveNew_NoCustomField_WhenAllIsSetupCorrectly_ExpectSuccessResponse( [Frozen] IMembersUserManager umbracoMembersUserManager, IMemberService memberService, IMemberTypeService memberTypeService, @@ -135,6 +134,45 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers AssertMemberDisplayPropertiesAreEqual(memberDisplay, result.Value); } + [Test] + [AutoMoqData] + public async Task PostSaveMember_SaveNew_CustomField_WhenAllIsSetupCorrectly_ExpectSuccessResponse( + [Frozen] IMembersUserManager umbracoMembersUserManager, + IMemberService memberService, + IMemberTypeService memberTypeService, + IMemberGroupService memberGroupService, + IDataTypeService dataTypeService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IBackOfficeSecurity backOfficeSecurity) + { + // arrange + Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew); + Mock.Get(umbracoMembersUserManager) + .Setup(x => x.CreateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(() => IdentityResult.Success); + Mock.Get(umbracoMembersUserManager) + .Setup(x => x.ValidatePasswordAsync(It.IsAny())) + .ReturnsAsync(() => IdentityResult.Success); + Mock.Get(memberTypeService).Setup(x => x.GetDefault()).Returns("fakeAlias"); + Mock.Get(backOfficeSecurityAccessor).Setup(x => x.BackOfficeSecurity).Returns(backOfficeSecurity); + Mock.Get(memberService).SetupSequence( + x => x.GetByEmail(It.IsAny())) + .Returns(() => null) + .Returns(() => member); + Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny())).Returns(() => member); + + MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor); + + // act + ActionResult result = await sut.PostSave(fakeMemberData); + + // assert + Assert.IsNull(result.Result); + Assert.IsNotNull(result.Value); + AssertMemberDisplayPropertiesAreEqual(memberDisplay, result.Value); + } + + [Test] [AutoMoqData] public async Task PostSaveMember_SaveExisting_WhenAllIsSetupCorrectly_ExpectSuccessResponse( @@ -220,6 +258,68 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers Assert.AreEqual(StatusCodes.Status400BadRequest, validation?.StatusCode); } + [Test] + [AutoMoqData] + public async Task PostSaveMember_SaveExistingMember_WithNoRoles_Add1Role_ExpectSuccessResponse( + [Frozen] IMembersUserManager umbracoMembersUserManager, + IMemberService memberService, + IMemberTypeService memberTypeService, + IMemberGroupService memberGroupService, + IDataTypeService dataTypeService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IBackOfficeSecurity backOfficeSecurity) + { + // arrange + string password = "fakepassword9aw89rnyco3938cyr^%&*()i8Y"; + var roleName = "anyrole"; + IMember member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.Save); + fakeMemberData.Groups = new List() + { + roleName + }; + var membersIdentityUser = new MembersIdentityUser(); + Mock.Get(umbracoMembersUserManager) + .Setup(x => x.FindByIdAsync(It.IsAny())) + .ReturnsAsync(() => membersIdentityUser); + Mock.Get(umbracoMembersUserManager) + .Setup(x => x.ValidatePasswordAsync(It.IsAny())) + .ReturnsAsync(() => IdentityResult.Success); + Mock.Get(umbracoMembersUserManager) + .Setup(x => x.HashPassword(It.IsAny())) + .Returns(password); + Mock.Get(umbracoMembersUserManager) + .Setup(x => x.UpdateAsync(It.IsAny())) + .ReturnsAsync(() => IdentityResult.Success); + Mock.Get(memberTypeService).Setup(x => x.GetDefault()).Returns("fakeAlias"); + Mock.Get(backOfficeSecurityAccessor).Setup(x => x.BackOfficeSecurity).Returns(backOfficeSecurity); + Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny())).Returns(() => member); + + Mock.Get(memberService).SetupSequence( + x => x.GetByEmail(It.IsAny())) + .Returns(() => null) + .Returns(() => member); + Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny())).Returns(() => member); + MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor); + + // act + ActionResult result = await sut.PostSave(fakeMemberData); + + // assert + Assert.IsNull(result.Result); + Assert.IsNotNull(result.Value); + Mock.Get(umbracoMembersUserManager) + .Verify(u => u.GetRolesAsync(membersIdentityUser)); + //Mock.Get(umbracoMembersUserManager) + // .Verify(u => u.RemoveFromRolesAsync(membersIdentityUser, new[] { "roles" })); + Mock.Get(umbracoMembersUserManager) + .Verify(u => u.AddToRolesAsync(membersIdentityUser, new[] { roleName })); + Mock.Get(memberService) + .Verify(m => m.Save(member, false)); + //Mock.Get(memberService) + // .Verify(m => m.AssignRoles(new[] { member.Username }, new[] { roleName })); + AssertMemberDisplayPropertiesAreEqual(memberDisplay, result.Value); + } + /// /// Create member controller to test /// @@ -428,12 +528,12 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers //Assert.AreEqual(memberDisplay.UpdateDate, resultValue.UpdateDate); //TODO: check all properties - //Assert.AreEqual(memberDisplay.Properties.Count(), resultValue.Properties.Count()); - //for (var index = 0; index < resultValue.Properties.Count(); index++) - //{ - // Assert.AreNotSame(memberDisplay.Properties.GetItemByIndex(index), resultValue.Properties.GetItemByIndex(index)); - // Assert.AreEqual(memberDisplay.Properties.GetItemByIndex(index), resultValue.Properties.GetItemByIndex(index)); - //} + Assert.AreEqual(memberDisplay.Properties.Count(), resultValue.Properties.Count()); + for (var index = 0; index < resultValue.Properties.Count(); index++) + { + Assert.AreNotSame(memberDisplay.Properties.GetItemByIndex(index), resultValue.Properties.GetItemByIndex(index)); + Assert.AreEqual(memberDisplay.Properties.GetItemByIndex(index), resultValue.Properties.GetItemByIndex(index)); + } } } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index 5e96c5936f..c58b9aa1ee 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -55,6 +55,7 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly ILocalizedTextService _localizedTextService; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; private readonly IJsonSerializer _jsonSerializer; + private readonly IShortStringHelper _shortStringHelper; /// /// Initializes a new instance of the class. @@ -97,6 +98,7 @@ namespace Umbraco.Web.BackOffice.Controllers _localizedTextService = localizedTextService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; _jsonSerializer = jsonSerializer; + _shortStringHelper = shortStringHelper; } /// @@ -358,14 +360,27 @@ namespace Umbraco.Web.BackOffice.Controllers return new ValidationErrorResult(created.Errors.ToErrorMessage()); } - // now re-look the member back up which will now exist + // now re-look up the member, which will now exist IMember member = _memberService.GetByEmail(contentItem.Email); + // map the save info over onto the user + member = _umbracoMapper.Map(contentItem, member); + int creatorId = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id; member.CreatorId = creatorId; - // map the save info over onto the user - member = _umbracoMapper.Map(contentItem, member); + // assign the mapped property values that are not part of the identity properties + string[] builtInAliases = ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper).Select(x => x.Key).ToArray(); + foreach (ContentPropertyBasic property in contentItem.Properties) + { + if (builtInAliases.Contains(property.Alias) == false) + { + member.Properties[property.Alias].SetValue(property.Value); + } + } + + //TODO: do we need to resave the key? + //contentItem.PersistedContent.Key = contentItem.Key; // now the member has been saved via identity, resave the member with mapped content properties _memberService.Save(member); From 8a9c3d0c2e4cdaaedffd8e4eb96c4219d8fc5ddc Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Thu, 4 Feb 2021 17:01:21 +0000 Subject: [PATCH 52/72] Fix tests (although all properties aren't checked) --- .../Controllers/MemberControllerUnitTests.cs | 53 ++++++++++++++----- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs index 2d7e035920..c3d3657a16 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs @@ -309,14 +309,10 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers Assert.IsNotNull(result.Value); Mock.Get(umbracoMembersUserManager) .Verify(u => u.GetRolesAsync(membersIdentityUser)); - //Mock.Get(umbracoMembersUserManager) - // .Verify(u => u.RemoveFromRolesAsync(membersIdentityUser, new[] { "roles" })); - Mock.Get(umbracoMembersUserManager) + Mock.Get(umbracoMembersUserManager) .Verify(u => u.AddToRolesAsync(membersIdentityUser, new[] { roleName })); Mock.Get(memberService) - .Verify(m => m.Save(member, false)); - //Mock.Get(memberService) - // .Verify(m => m.AssignRoles(new[] { member.Username }, new[] { roleName })); + .Verify(m => m.Save(It.IsAny(), true)); AssertMemberDisplayPropertiesAreEqual(memberDisplay, result.Value); } @@ -454,11 +450,6 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers PersistedContent = member, PropertyCollectionDto = new ContentPropertyCollectionDto() { - Properties = new List() - { - new ContentPropertyDto(), - new ContentPropertyDto() - } }, Groups = new List(), //Alias = "fakeAlias", @@ -487,7 +478,41 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers { new Tab() { - Alias = "test" + Alias = "test", + Id = 77, + Properties = new List() + { + new ContentPropertyDisplay() + { + Alias = "_umb_id", + View = "idwithguid", + Value = new [] + { + "123", + "guid" + } + }, + new ContentPropertyDisplay() + { + Alias = "_umb_doctype" + }, + new ContentPropertyDisplay() + { + Alias = "_umb_login" + }, + new ContentPropertyDisplay() + { + Alias= "_umb_email" + }, + new ContentPropertyDisplay() + { + Alias = "_umb_password" + }, + new ContentPropertyDisplay() + { + Alias = "_umb_membergroup" + } + } } } }; @@ -521,7 +546,6 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers Assert.AreEqual(memberDisplay.SortOrder, resultValue.SortOrder); Assert.AreEqual(memberDisplay.Trashed, resultValue.Trashed); Assert.AreEqual(memberDisplay.TreeNodeUrl, resultValue.TreeNodeUrl); - Assert.AreNotSame(memberDisplay.Properties, resultValue.Properties); //TODO: can we check create/update dates when saving? //Assert.AreEqual(memberDisplay.CreateDate, resultValue.CreateDate); @@ -529,10 +553,11 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers //TODO: check all properties Assert.AreEqual(memberDisplay.Properties.Count(), resultValue.Properties.Count()); + Assert.AreNotSame(memberDisplay.Properties, resultValue.Properties); for (var index = 0; index < resultValue.Properties.Count(); index++) { Assert.AreNotSame(memberDisplay.Properties.GetItemByIndex(index), resultValue.Properties.GetItemByIndex(index)); - Assert.AreEqual(memberDisplay.Properties.GetItemByIndex(index), resultValue.Properties.GetItemByIndex(index)); + //Assert.AreEqual(memberDisplay.Properties.GetItemByIndex(index), resultValue.Properties.GetItemByIndex(index)); } } } From 5d51427858df950730771da0fe270a8df51a2b6f Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Tue, 9 Feb 2021 13:45:08 +0000 Subject: [PATCH 53/72] PR Review: Renamed to Noop for consistency --- .../Security/NoOpLookupNormalizer.cs | 4 +--- src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs | 2 +- .../Security/NoOpLookupNormalizerTests.cs | 8 ++++---- .../DependencyInjection/ServiceCollectionExtensions.cs | 4 ++-- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Infrastructure/Security/NoOpLookupNormalizer.cs b/src/Umbraco.Infrastructure/Security/NoOpLookupNormalizer.cs index c81a46e726..7c114835d7 100644 --- a/src/Umbraco.Infrastructure/Security/NoOpLookupNormalizer.cs +++ b/src/Umbraco.Infrastructure/Security/NoOpLookupNormalizer.cs @@ -6,10 +6,8 @@ namespace Umbraco.Infrastructure.Security /// /// No-op lookup normalizer to maintain compatibility with ASP.NET Identity 2 /// - public class NoOpLookupNormalizer : ILookupNormalizer + public class NoopLookupNormalizer : ILookupNormalizer { - // TODO: Do we need this? - public string NormalizeName(string name) => name; public string NormalizeEmail(string email) => email; diff --git a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs index 7c767865b0..43155b4567 100644 --- a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs @@ -38,7 +38,7 @@ namespace Umbraco.Infrastructure.Security IServiceProvider services, ILogger> logger, IOptions passwordConfiguration) - : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, new NoOpLookupNormalizer(), errors, services, logger) + : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, new NoopLookupNormalizer(), errors, services, logger) { IpResolver = ipResolver ?? throw new ArgumentNullException(nameof(ipResolver)); PasswordConfiguration = passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration)); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/NoOpLookupNormalizerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/NoOpLookupNormalizerTests.cs index 86cb339625..27202e9353 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/NoOpLookupNormalizerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/NoOpLookupNormalizerTests.cs @@ -13,7 +13,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Security public void NormalizeName_Expect_Input_Returned() { var name = Guid.NewGuid().ToString(); - var sut = new NoOpLookupNormalizer(); + var sut = new NoopLookupNormalizer(); var normalizedName = sut.NormalizeName(name); @@ -24,7 +24,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Security public void NormalizeEmail_Expect_Input_Returned() { var email = $"{Guid.NewGuid()}@umbraco"; - var sut = new NoOpLookupNormalizer(); + var sut = new NoopLookupNormalizer(); var normalizedEmail = sut.NormalizeEmail(email); @@ -37,7 +37,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Security [TestCase(" ")] public void NormalizeName_When_Name_Null_Or_Whitespace_Expect_Same_Returned(string name) { - var sut = new NoOpLookupNormalizer(); + var sut = new NoopLookupNormalizer(); var normalizedName = sut.NormalizeName(name); @@ -50,7 +50,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Security [TestCase(" ")] public void NormalizeEmail_When_Name_Null_Or_Whitespace_Expect_Same_Returned(string email) { - var sut = new NoOpLookupNormalizer(); + var sut = new NoopLookupNormalizer(); var normalizedEmail = sut.NormalizeEmail(email); diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs index bca0c67ed7..56ab6a904d 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs @@ -67,7 +67,7 @@ namespace Umbraco.Web.BackOffice.DependencyInjection services.TryAddScoped, UserClaimsPrincipalFactory>(); // CUSTOM: - services.TryAddScoped(); + services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); services.TryAddSingleton(); @@ -79,7 +79,7 @@ namespace Umbraco.Web.BackOffice.DependencyInjection * To validate the container the following registrations are required (dependencies of UserManager) * Perhaps we shouldn't be registering UserManager at all and only registering/depending the UmbracoBackOffice prefixed types. */ - services.TryAddScoped(); + services.TryAddScoped(); services.TryAddScoped(); return new BackOfficeIdentityBuilder(services); From da2a0713cc402d1274f4825a6e423197c41474d1 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Tue, 9 Feb 2021 15:08:02 +0000 Subject: [PATCH 54/72] Removed unwanted method to use property instead --- .../Security/UmbracoUserManager.cs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs index 43155b4567..04560fe45b 100644 --- a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs @@ -89,13 +89,6 @@ namespace Umbraco.Infrastructure.Security return await userSessionStore.ValidateSessionIdAsync(userId, sessionId); } - /// - /// This will determine which password hasher to use based on what is defined in config - /// - /// The - /// An - protected virtual IPasswordHasher GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration) => new PasswordHasher(); - /// /// Helper method to generate a password for a user based on the current password validator /// @@ -116,9 +109,7 @@ namespace Umbraco.Infrastructure.Security /// The hashed password public string HashPassword(string password) { - IPasswordHasher passwordHasher = GetDefaultPasswordHasher(PasswordConfiguration); - - string hashedPassword = passwordHasher.HashPassword(null, password); + string hashedPassword = PasswordHasher.HashPassword(null, password); return hashedPassword; } From 093548b933d78a238d85ca56232a4c045a4e648a Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Tue, 9 Feb 2021 16:14:32 +0000 Subject: [PATCH 55/72] Moved EmailConfirmed and SecurityStamp properties to shared IMembershipUser (for both IMember and IUser to use) --- src/Umbraco.Core/Models/Member.cs | 117 +++++++++++++----- .../Models/Membership/IMembershipUser.cs | 8 +- src/Umbraco.Core/Models/Membership/IUser.cs | 8 +- .../Security/IdentityMapDefinition.cs | 4 +- .../Security/MembersUserStore.cs | 24 ++-- 5 files changed, 107 insertions(+), 54 deletions(-) diff --git a/src/Umbraco.Core/Models/Member.cs b/src/Umbraco.Core/Models/Member.cs index 8a765b2f25..7a3b2fd20f 100644 --- a/src/Umbraco.Core/Models/Member.cs +++ b/src/Umbraco.Core/Models/Member.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Runtime.Serialization; @@ -19,8 +19,11 @@ namespace Umbraco.Core.Models private string _email; private string _rawPasswordValue; private string _passwordConfig; + private DateTime? _emailConfirmedDate; + private string _securityStamp; /// + /// Initializes a new instance of the class. /// Constructor for creating an empty Member object /// /// ContentType for the current Content object @@ -29,13 +32,14 @@ namespace Umbraco.Core.Models { IsApproved = true; - //this cannot be null but can be empty + // this cannot be null but can be empty _rawPasswordValue = ""; _email = ""; _username = ""; } /// + /// Initializes a new instance of the class. /// Constructor for creating a Member object /// /// Name of the content @@ -43,18 +47,21 @@ namespace Umbraco.Core.Models public Member(string name, IMemberType contentType) : base(name, -1, contentType, new PropertyCollection()) { - if (name == null) throw new ArgumentNullException(nameof(name)); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); + if (name == null) + throw new ArgumentNullException(nameof(name)); + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); IsApproved = true; - //this cannot be null but can be empty + // this cannot be null but can be empty _rawPasswordValue = ""; _email = ""; _username = ""; } /// + /// Initializes a new instance of the class. /// Constructor for creating a Member object /// /// @@ -64,22 +71,29 @@ namespace Umbraco.Core.Models public Member(string name, string email, string username, IMemberType contentType, bool isApproved = true) : base(name, -1, contentType, new PropertyCollection()) { - if (name == null) throw new ArgumentNullException(nameof(name)); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); - if (email == null) throw new ArgumentNullException(nameof(email)); - if (string.IsNullOrWhiteSpace(email)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(email)); - if (username == null) throw new ArgumentNullException(nameof(username)); - if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(username)); + if (name == null) + throw new ArgumentNullException(nameof(name)); + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); + if (email == null) + throw new ArgumentNullException(nameof(email)); + if (string.IsNullOrWhiteSpace(email)) + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(email)); + if (username == null) + throw new ArgumentNullException(nameof(username)); + if (string.IsNullOrWhiteSpace(username)) + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(username)); _email = email; _username = username; IsApproved = isApproved; - //this cannot be null but can be empty + // this cannot be null but can be empty _rawPasswordValue = ""; } /// + /// Initializes a new instance of the class. /// Constructor for creating a Member object /// /// @@ -99,6 +113,7 @@ namespace Umbraco.Core.Models } /// + /// Initializes a new instance of the class. /// Constructor for creating a Member object /// /// @@ -138,6 +153,13 @@ namespace Umbraco.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _email, nameof(Email)); } + [DataMember] + public DateTime? EmailConfirmedDate + { + get => _emailConfirmedDate; + set => SetPropertyValueAndDetectChanges(value, ref _emailConfirmedDate, nameof(EmailConfirmedDate)); + } + /// /// Gets or sets the raw password value /// @@ -190,7 +212,8 @@ namespace Umbraco.Core.Models get { var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.Comments, nameof(Comments), default(string)); - if (a.Success == false) return a.Result; + if (a.Success == false) + return a.Result; return Properties[Constants.Conventions.Member.Comments].GetValue() == null ? string.Empty @@ -200,7 +223,8 @@ namespace Umbraco.Core.Models { if (WarnIfPropertyTypeNotFoundOnSet( Constants.Conventions.Member.Comments, - nameof(Comments)) == false) return; + nameof(Comments)) == false) + return; Properties[Constants.Conventions.Member.Comments].SetValue(value); } @@ -221,8 +245,10 @@ namespace Umbraco.Core.Models var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.IsApproved, nameof(IsApproved), //This is the default value if the prop is not found true); - if (a.Success == false) return a.Result; - if (Properties[Constants.Conventions.Member.IsApproved].GetValue() == null) return true; + if (a.Success == false) + return a.Result; + if (Properties[Constants.Conventions.Member.IsApproved].GetValue() == null) + return true; var tryConvert = Properties[Constants.Conventions.Member.IsApproved].GetValue().TryConvertTo(); if (tryConvert.Success) { @@ -235,7 +261,8 @@ namespace Umbraco.Core.Models { if (WarnIfPropertyTypeNotFoundOnSet( Constants.Conventions.Member.IsApproved, - nameof(IsApproved)) == false) return; + nameof(IsApproved)) == false) + return; Properties[Constants.Conventions.Member.IsApproved].SetValue(value); } @@ -254,8 +281,10 @@ namespace Umbraco.Core.Models get { var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.IsLockedOut, nameof(IsLockedOut), false); - if (a.Success == false) return a.Result; - if (Properties[Constants.Conventions.Member.IsLockedOut].GetValue() == null) return false; + if (a.Success == false) + return a.Result; + if (Properties[Constants.Conventions.Member.IsLockedOut].GetValue() == null) + return false; var tryConvert = Properties[Constants.Conventions.Member.IsLockedOut].GetValue().TryConvertTo(); if (tryConvert.Success) { @@ -268,7 +297,8 @@ namespace Umbraco.Core.Models { if (WarnIfPropertyTypeNotFoundOnSet( Constants.Conventions.Member.IsLockedOut, - nameof(IsLockedOut)) == false) return; + nameof(IsLockedOut)) == false) + return; Properties[Constants.Conventions.Member.IsLockedOut].SetValue(value); } @@ -287,8 +317,10 @@ namespace Umbraco.Core.Models get { var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.LastLoginDate, nameof(LastLoginDate), default(DateTime)); - if (a.Success == false) return a.Result; - if (Properties[Constants.Conventions.Member.LastLoginDate].GetValue() == null) return default(DateTime); + if (a.Success == false) + return a.Result; + if (Properties[Constants.Conventions.Member.LastLoginDate].GetValue() == null) + return default(DateTime); var tryConvert = Properties[Constants.Conventions.Member.LastLoginDate].GetValue().TryConvertTo(); if (tryConvert.Success) { @@ -301,7 +333,8 @@ namespace Umbraco.Core.Models { if (WarnIfPropertyTypeNotFoundOnSet( Constants.Conventions.Member.LastLoginDate, - nameof(LastLoginDate)) == false) return; + nameof(LastLoginDate)) == false) + return; Properties[Constants.Conventions.Member.LastLoginDate].SetValue(value); } @@ -320,8 +353,10 @@ namespace Umbraco.Core.Models get { var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.LastPasswordChangeDate, nameof(LastPasswordChangeDate), default(DateTime)); - if (a.Success == false) return a.Result; - if (Properties[Constants.Conventions.Member.LastPasswordChangeDate].GetValue() == null) return default(DateTime); + if (a.Success == false) + return a.Result; + if (Properties[Constants.Conventions.Member.LastPasswordChangeDate].GetValue() == null) + return default(DateTime); var tryConvert = Properties[Constants.Conventions.Member.LastPasswordChangeDate].GetValue().TryConvertTo(); if (tryConvert.Success) { @@ -334,7 +369,8 @@ namespace Umbraco.Core.Models { if (WarnIfPropertyTypeNotFoundOnSet( Constants.Conventions.Member.LastPasswordChangeDate, - nameof(LastPasswordChangeDate)) == false) return; + nameof(LastPasswordChangeDate)) == false) + return; Properties[Constants.Conventions.Member.LastPasswordChangeDate].SetValue(value); } @@ -353,8 +389,10 @@ namespace Umbraco.Core.Models get { var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.LastLockoutDate, nameof(LastLockoutDate), default(DateTime)); - if (a.Success == false) return a.Result; - if (Properties[Constants.Conventions.Member.LastLockoutDate].GetValue() == null) return default(DateTime); + if (a.Success == false) + return a.Result; + if (Properties[Constants.Conventions.Member.LastLockoutDate].GetValue() == null) + return default(DateTime); var tryConvert = Properties[Constants.Conventions.Member.LastLockoutDate].GetValue().TryConvertTo(); if (tryConvert.Success) { @@ -367,7 +405,8 @@ namespace Umbraco.Core.Models { if (WarnIfPropertyTypeNotFoundOnSet( Constants.Conventions.Member.LastLockoutDate, - nameof(LastLockoutDate)) == false) return; + nameof(LastLockoutDate)) == false) + return; Properties[Constants.Conventions.Member.LastLockoutDate].SetValue(value); } @@ -387,8 +426,10 @@ namespace Umbraco.Core.Models get { var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.FailedPasswordAttempts, nameof(FailedPasswordAttempts), 0); - if (a.Success == false) return a.Result; - if (Properties[Constants.Conventions.Member.FailedPasswordAttempts].GetValue() == null) return default(int); + if (a.Success == false) + return a.Result; + if (Properties[Constants.Conventions.Member.FailedPasswordAttempts].GetValue() == null) + return default(int); var tryConvert = Properties[Constants.Conventions.Member.FailedPasswordAttempts].GetValue().TryConvertTo(); if (tryConvert.Success) { @@ -401,7 +442,8 @@ namespace Umbraco.Core.Models { if (WarnIfPropertyTypeNotFoundOnSet( Constants.Conventions.Member.FailedPasswordAttempts, - nameof(FailedPasswordAttempts)) == false) return; + nameof(FailedPasswordAttempts)) == false) + return; Properties[Constants.Conventions.Member.FailedPasswordAttempts].SetValue(value); } @@ -413,6 +455,17 @@ namespace Umbraco.Core.Models [DataMember] public virtual string ContentTypeAlias => ContentType.Alias; + /// + /// The security stamp used by ASP.Net identity + /// + [IgnoreDataMember] + public string SecurityStamp + { + get => _securityStamp; + set => SetPropertyValueAndDetectChanges(value, ref _securityStamp, nameof(SecurityStamp)); + } + + /// /// Internal/Experimental - only used for mapping queries. /// diff --git a/src/Umbraco.Core/Models/Membership/IMembershipUser.cs b/src/Umbraco.Core/Models/Membership/IMembershipUser.cs index 9b1c8a0c07..c8ecc4b3c6 100644 --- a/src/Umbraco.Core/Models/Membership/IMembershipUser.cs +++ b/src/Umbraco.Core/Models/Membership/IMembershipUser.cs @@ -1,4 +1,4 @@ -using System; +using System; using Umbraco.Core.Models.Entities; namespace Umbraco.Core.Models.Membership @@ -10,6 +10,7 @@ namespace Umbraco.Core.Models.Membership { string Username { get; set; } string Email { get; set; } + DateTime? EmailConfirmedDate { get; set; } /// /// Gets or sets the raw password value @@ -38,6 +39,11 @@ namespace Umbraco.Core.Models.Membership /// int FailedPasswordAttempts { get; set; } + /// + /// Gets or sets the security stamp used by ASP.NET Identity + /// + string SecurityStamp { get; set; } + //object ProfileId { get; set; } //IEnumerable Groups { get; set; } } diff --git a/src/Umbraco.Core/Models/Membership/IUser.cs b/src/Umbraco.Core/Models/Membership/IUser.cs index 3a3a18b5ab..1554c3cef5 100644 --- a/src/Umbraco.Core/Models/Membership/IUser.cs +++ b/src/Umbraco.Core/Models/Membership/IUser.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel; using Umbraco.Core.Models.Entities; @@ -20,7 +20,6 @@ namespace Umbraco.Core.Models.Membership int[] StartMediaIds { get; set; } string Language { get; set; } - DateTime? EmailConfirmedDate { get; set; } DateTime? InvitedDate { get; set; } /// @@ -39,11 +38,6 @@ namespace Umbraco.Core.Models.Membership /// IProfile ProfileData { get; } - /// - /// The security stamp used by ASP.Net identity - /// - string SecurityStamp { get; set; } - /// /// Will hold the media file system relative path of the users custom avatar if they uploaded one /// diff --git a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs index e79d346c8a..6477c184c6 100644 --- a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs +++ b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs @@ -96,13 +96,13 @@ namespace Umbraco.Infrastructure.Security target.UserName = source.Username; target.LastPasswordChangeDateUtc = source.LastPasswordChangeDate.ToUniversalTime(); target.LastLoginDateUtc = source.LastLoginDate.ToUniversalTime(); - //target.EmailConfirmed = source.EmailConfirmedDate.HasValue; + target.EmailConfirmed = source.EmailConfirmedDate.HasValue; target.Name = source.Name; target.AccessFailedCount = source.FailedPasswordAttempts; target.PasswordHash = GetPasswordHash(source.RawPasswordValue); target.PasswordConfig = source.PasswordConfiguration; target.IsApproved = source.IsApproved; - //target.SecurityStamp = source.SecurityStamp; + target.SecurityStamp = source.SecurityStamp; target.LockoutEnd = source.IsLockedOut ? DateTime.MaxValue.ToUniversalTime() : (DateTime?)null; // NB: same comments re AutoMapper as per BackOfficeUser diff --git a/src/Umbraco.Infrastructure/Security/MembersUserStore.cs b/src/Umbraco.Infrastructure/Security/MembersUserStore.cs index 744919cfa1..7690eed210 100644 --- a/src/Umbraco.Infrastructure/Security/MembersUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MembersUserStore.cs @@ -554,13 +554,13 @@ namespace Umbraco.Infrastructure.Security member.LastPasswordChangeDate = identityUserMember.LastPasswordChangeDateUtc.Value.ToLocalTime(); } - //if (identityUser.IsPropertyDirty(nameof(MembersIdentityUser.EmailConfirmed)) - // || (user.EmailConfirmedDate.HasValue && user.EmailConfirmedDate.Value != default && identityUser.EmailConfirmed == false) - // || ((user.EmailConfirmedDate.HasValue == false || user.EmailConfirmedDate.Value == default) && identityUser.EmailConfirmed)) - //{ - // anythingChanged = true; - // user.EmailConfirmedDate = identityUser.EmailConfirmed ? (DateTime?)DateTime.Now : null; - //} + if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.EmailConfirmed)) + || (member.EmailConfirmedDate.HasValue && member.EmailConfirmedDate.Value != default && identityUserMember.EmailConfirmed == false) + || ((member.EmailConfirmedDate.HasValue == false || member.EmailConfirmedDate.Value == default) && identityUserMember.EmailConfirmed)) + { + anythingChanged = true; + member.EmailConfirmedDate = identityUserMember.EmailConfirmed ? (DateTime?)DateTime.Now : null; + } if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.Name)) && member.Name != identityUserMember.Name && identityUserMember.Name.IsNullOrWhiteSpace() == false) @@ -610,11 +610,11 @@ namespace Umbraco.Infrastructure.Security member.PasswordConfiguration = identityUserMember.PasswordConfig; } - //if (user.SecurityStamp != identityUser.SecurityStamp) - //{ - // anythingChanged = true; - // user.SecurityStamp = identityUser.SecurityStamp; - //} + if (member.SecurityStamp != identityUserMember.SecurityStamp) + { + anythingChanged = true; + member.SecurityStamp = identityUserMember.SecurityStamp; + } // TODO: Fix this for Groups too (as per backoffice comment) if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.Roles)) || identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.Groups))) From 89ef1aab3711b8fd81347a6b456d4047686bc04a Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Wed, 10 Feb 2021 10:23:25 +0000 Subject: [PATCH 56/72] Renamed Noop --- .../Security/NoOpLookupNormalizerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/NoOpLookupNormalizerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/NoOpLookupNormalizerTests.cs index 27202e9353..1fc38cf787 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/NoOpLookupNormalizerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/NoOpLookupNormalizerTests.cs @@ -7,7 +7,7 @@ using Umbraco.Infrastructure.Security; namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Security { - public class NoOpLookupNormalizerTests + public class NoopLookupNormalizerTests { [Test] public void NormalizeName_Expect_Input_Returned() From 2d00ece365e67d9932e573ab27fb520422703fa2 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Fri, 12 Feb 2021 14:38:20 +0000 Subject: [PATCH 57/72] Renamed MembersUserManager --- .../{IMembersUserManager.cs => IMemberManager.cs} | 2 +- .../MembersServiceCollectionExtensionsTests.cs | 2 +- .../AutoFixture/AutoMoqDataAttribute.cs | 2 +- .../Security/MemberIdentityUserManagerTests.cs | 10 +++++----- .../Controllers/MemberControllerUnitTests.cs | 14 +++++++------- .../Controllers/MemberController.cs | 4 ++-- .../ServiceCollectionExtensions.cs | 2 +- .../{MembersUserManager.cs => MemberManager.cs} | 4 ++-- 8 files changed, 20 insertions(+), 20 deletions(-) rename src/Umbraco.Infrastructure/Security/{IMembersUserManager.cs => IMemberManager.cs} (61%) rename src/Umbraco.Web.Common/Security/{MembersUserManager.cs => MemberManager.cs} (94%) diff --git a/src/Umbraco.Infrastructure/Security/IMembersUserManager.cs b/src/Umbraco.Infrastructure/Security/IMemberManager.cs similarity index 61% rename from src/Umbraco.Infrastructure/Security/IMembersUserManager.cs rename to src/Umbraco.Infrastructure/Security/IMemberManager.cs index a466ea2aa9..47d8a4f2fb 100644 --- a/src/Umbraco.Infrastructure/Security/IMembersUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/IMemberManager.cs @@ -3,7 +3,7 @@ namespace Umbraco.Infrastructure.Security /// /// The user manager for members /// - public interface IMembersUserManager : IUmbracoUserManager + public interface IMemberManager : IUmbracoUserManager { } } diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/MembersServiceCollectionExtensionsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/MembersServiceCollectionExtensionsTests.cs index 8dd4bee0cf..dc730201fe 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/MembersServiceCollectionExtensionsTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/MembersServiceCollectionExtensionsTests.cs @@ -25,7 +25,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Web.BackOffice [Test] public void AddMembersIdentity_ExpectMembersUserManagerResolvable() { - IMembersUserManager userManager = Services.GetService(); + IMemberManager userManager = Services.GetService(); Assert.NotNull(userManager); } diff --git a/src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs b/src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs index ca95c73345..589f2572aa 100644 --- a/src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs +++ b/src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs @@ -55,7 +55,7 @@ namespace Umbraco.Tests.UnitTests.AutoFixture .Customize(new ConstructorCustomization(typeof(MemberController), new GreedyConstructorQuery())) .Customize(new ConstructorCustomization(typeof(BackOfficeController), new GreedyConstructorQuery())) .Customize(new ConstructorCustomization(typeof(BackOfficeUserManager), new GreedyConstructorQuery())) - .Customize(new ConstructorCustomization(typeof(MembersUserManager), new GreedyConstructorQuery())); + .Customize(new ConstructorCustomization(typeof(MemberManager), new GreedyConstructorQuery())); fixture.Customize(new AutoMoqCustomization()); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserManagerTests.cs index 438f6d35bc..dd90a71180 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserManagerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserManagerTests.cs @@ -31,7 +31,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Security private Mock>> _mockLogger; private Mock> _mockPasswordConfiguration; - public MembersUserManager CreateSut() + public MemberManager CreateSut() { _mockMemberStore = new Mock>(); _mockIdentityOptions = new Mock>(); @@ -62,7 +62,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Security new PasswordValidator() }; - var userManager = new MembersUserManager( + var userManager = new MemberManager( new Mock().Object, _mockMemberStore.Object, _mockIdentityOptions.Object, @@ -87,7 +87,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenICreateUser_AndTheIdentityResultFailed_ThenIShouldGetAFailedResultAsync() { //arrange - MembersUserManager sut = CreateSut(); + MemberManager sut = CreateSut(); MembersIdentityUser fakeUser = new MembersIdentityUser() { PasswordConfig = "testConfig" @@ -120,7 +120,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenICreateUser_AndTheUserIsNull_ThenIShouldGetAFailedResultAsync() { //arrange - MembersUserManager sut = CreateSut(); + MemberManager sut = CreateSut(); CancellationToken fakeCancellationToken = new CancellationToken() { }; IdentityError[] identityErrors = { @@ -148,7 +148,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenICreateANewUser_AndTheUserIsPopulatedCorrectly_ThenIShouldGetASuccessResultAsync() { //arrange - MembersUserManager sut = CreateSut(); + MemberManager sut = CreateSut(); MembersIdentityUser fakeUser = new MembersIdentityUser() { PasswordConfig = "testConfig" diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs index c3d3657a16..c69d1365ed 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs @@ -64,7 +64,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers [Test] [AutoMoqData] public void PostSaveMember_WhenModelStateIsNotValid_ExpectFailureResponse( - [Frozen] IMembersUserManager umbracoMembersUserManager, + [Frozen] IMemberManager umbracoMembersUserManager, IMemberService memberService, IMemberTypeService memberTypeService, IMemberGroupService memberGroupService, @@ -99,7 +99,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers [Test] [AutoMoqData] public async Task PostSaveMember_SaveNew_NoCustomField_WhenAllIsSetupCorrectly_ExpectSuccessResponse( - [Frozen] IMembersUserManager umbracoMembersUserManager, + [Frozen] IMemberManager umbracoMembersUserManager, IMemberService memberService, IMemberTypeService memberTypeService, IMemberGroupService memberGroupService, @@ -137,7 +137,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers [Test] [AutoMoqData] public async Task PostSaveMember_SaveNew_CustomField_WhenAllIsSetupCorrectly_ExpectSuccessResponse( - [Frozen] IMembersUserManager umbracoMembersUserManager, + [Frozen] IMemberManager umbracoMembersUserManager, IMemberService memberService, IMemberTypeService memberTypeService, IMemberGroupService memberGroupService, @@ -176,7 +176,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers [Test] [AutoMoqData] public async Task PostSaveMember_SaveExisting_WhenAllIsSetupCorrectly_ExpectSuccessResponse( - [Frozen] IMembersUserManager umbracoMembersUserManager, + [Frozen] IMemberManager umbracoMembersUserManager, IMemberService memberService, IMemberTypeService memberTypeService, IMemberGroupService memberGroupService, @@ -222,7 +222,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers [Test] [AutoMoqData] public void PostSaveMember_SaveNew_WhenMemberEmailAlreadyExists_ExpectFailResponse( - [Frozen] IMembersUserManager umbracoMembersUserManager, + [Frozen] IMemberManager umbracoMembersUserManager, IMemberService memberService, IMemberTypeService memberTypeService, IMemberGroupService memberGroupService, @@ -261,7 +261,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers [Test] [AutoMoqData] public async Task PostSaveMember_SaveExistingMember_WithNoRoles_Add1Role_ExpectSuccessResponse( - [Frozen] IMembersUserManager umbracoMembersUserManager, + [Frozen] IMemberManager umbracoMembersUserManager, IMemberService memberService, IMemberTypeService memberTypeService, IMemberGroupService memberGroupService, @@ -330,7 +330,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers IMemberService memberService, IMemberTypeService memberTypeService, IMemberGroupService memberGroupService, - IMembersUserManager membersUserManager, + IMemberManager membersUserManager, IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) { diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index c58b9aa1ee..fa8788f14d 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -50,7 +50,7 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly UmbracoMapper _umbracoMapper; private readonly IMemberService _memberService; private readonly IMemberTypeService _memberTypeService; - private readonly IMembersUserManager _memberManager; + private readonly IMemberManager _memberManager; private readonly IDataTypeService _dataTypeService; private readonly ILocalizedTextService _localizedTextService; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; @@ -83,7 +83,7 @@ namespace Umbraco.Web.BackOffice.Controllers UmbracoMapper umbracoMapper, IMemberService memberService, IMemberTypeService memberTypeService, - IMembersUserManager memberManager, + IMemberManager memberManager, IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IJsonSerializer jsonSerializer) diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs index 56ab6a904d..4aa6fc96c1 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs @@ -45,7 +45,7 @@ namespace Umbraco.Web.BackOffice.DependencyInjection services.BuildMembersIdentity() .AddDefaultTokenProviders() .AddUserStore() - .AddMembersUserManager(); + .AddMembersUserManager(); private static BackOfficeIdentityBuilder BuildUmbracoBackOfficeIdentity(this IServiceCollection services) { diff --git a/src/Umbraco.Web.Common/Security/MembersUserManager.cs b/src/Umbraco.Web.Common/Security/MemberManager.cs similarity index 94% rename from src/Umbraco.Web.Common/Security/MembersUserManager.cs rename to src/Umbraco.Web.Common/Security/MemberManager.cs index 51206bce61..a4c8ca10bd 100644 --- a/src/Umbraco.Web.Common/Security/MembersUserManager.cs +++ b/src/Umbraco.Web.Common/Security/MemberManager.cs @@ -17,11 +17,11 @@ using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Common.Security { - public class MembersUserManager : UmbracoUserManager, IMembersUserManager + public class MemberManager : UmbracoUserManager, IMemberManager { private readonly IHttpContextAccessor _httpContextAccessor; - public MembersUserManager( + public MemberManager( IIpResolver ipResolver, IUserStore store, IOptions optionsAccessor, From d508bd21ff4150c0b4a89a394f23839064ecf574 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Fri, 12 Feb 2021 17:03:53 +0000 Subject: [PATCH 58/72] Fixed PR review comments --- src/Umbraco.Infrastructure/Services/Implement/MemberService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs index 721c0eee21..4f3c77efb4 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs @@ -941,7 +941,7 @@ namespace Umbraco.Infrastructure.Services.Implement using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); - return _memberGroupRepository.GetMany().Select(x=>x).Distinct(); + return _memberGroupRepository.GetMany().Distinct(); } } From a4ee8055f941da4f42a1aef157814f8aecf50cb7 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Fri, 12 Feb 2021 17:06:29 +0000 Subject: [PATCH 59/72] Moved builder to corect location --- .../Testing/UmbracoIntegrationTest.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index 71ba3367fc..0e79cd6464 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -218,6 +218,7 @@ namespace Umbraco.Tests.Integration.Testing .AddRuntimeMinifier() .AddBackOfficeAuthentication() .AddBackOfficeIdentity() + .AddMembersIdentity() .AddTestServices(TestHelper, GetAppCaches()); if (TestOptions.Mapper) @@ -229,8 +230,6 @@ namespace Umbraco.Tests.Integration.Testing } services.AddSignalR(); - builder.AddMembersIdentity(); - services.AddMvc(); CustomTestSetup(builder); From 167811b23b9a788e00de557fb03220a13e6d5fab Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Sun, 14 Feb 2021 12:57:48 +0000 Subject: [PATCH 60/72] Moving adding members services to correct project for use on the front-end, not just the back-office. --- ...MembersServiceCollectionExtensionsTests.cs | 4 ++-- .../ServiceCollectionExtensions.cs | 19 +-------------- .../Extensions/IdentityBuilderExtensions.cs | 16 +------------ .../ServiceCollectionExtensions.cs | 24 +++++++++++++++++++ 4 files changed, 28 insertions(+), 35 deletions(-) rename src/Umbraco.Tests.Integration/{Umbraco.Web.BackOffice => Umbraco.Web.Common}/MembersServiceCollectionExtensionsTests.cs (90%) diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/MembersServiceCollectionExtensionsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.Common/MembersServiceCollectionExtensionsTests.cs similarity index 90% rename from src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/MembersServiceCollectionExtensionsTests.cs rename to src/Umbraco.Tests.Integration/Umbraco.Web.Common/MembersServiceCollectionExtensionsTests.cs index dc730201fe..5072d569e0 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/MembersServiceCollectionExtensionsTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.Common/MembersServiceCollectionExtensionsTests.cs @@ -4,9 +4,9 @@ using NUnit.Framework; using Umbraco.Core.DependencyInjection; using Umbraco.Infrastructure.Security; using Umbraco.Tests.Integration.Testing; -using Umbraco.Web.BackOffice.DependencyInjection; +using Umbraco.Web.Common.DependencyInjection; -namespace Umbraco.Tests.Integration.Umbraco.Web.BackOffice +namespace Umbraco.Tests.Integration.Umbraco.Web.Common { [TestFixture] public class MembersServiceCollectionExtensionsTests : UmbracoIntegrationTest diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs index 4aa6fc96c1..cea8a8fb2c 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs @@ -10,6 +10,7 @@ using Umbraco.Infrastructure.Security; using Umbraco.Net; using Umbraco.Web.Actions; using Umbraco.Web.BackOffice.Authorization; +using Umbraco.Web.BackOffice.Extensions; using Umbraco.Web.BackOffice.Security; using Umbraco.Web.Common.AspNetCore; using Umbraco.Web.Common.Authorization; @@ -38,15 +39,6 @@ namespace Umbraco.Web.BackOffice.DependencyInjection services.ConfigureOptions(); } - /// - /// Adds the services required for using Members Identity - /// - public static void AddMembersIdentity(this IServiceCollection services) => - services.BuildMembersIdentity() - .AddDefaultTokenProviders() - .AddUserStore() - .AddMembersUserManager(); - private static BackOfficeIdentityBuilder BuildUmbracoBackOfficeIdentity(this IServiceCollection services) { // Borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/IdentityServiceCollectionExtensions.cs#L33 @@ -85,15 +77,6 @@ namespace Umbraco.Web.BackOffice.DependencyInjection return new BackOfficeIdentityBuilder(services); } - private static MembersIdentityBuilder BuildMembersIdentity(this IServiceCollection services) - { - // Services used by Umbraco members identity - services.TryAddScoped, UserValidator>(); - services.TryAddScoped, PasswordValidator>(); - services.TryAddScoped, PasswordHasher>(); - return new MembersIdentityBuilder(services); - } - /// /// Add authorization handlers and policies /// diff --git a/src/Umbraco.Web.BackOffice/Extensions/IdentityBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/IdentityBuilderExtensions.cs index 7c576f2858..deb312d071 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/IdentityBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/IdentityBuilderExtensions.cs @@ -1,9 +1,8 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; -using Umbraco.Core.Security; using Umbraco.Infrastructure.Security; -namespace Umbraco.Extensions +namespace Umbraco.Web.BackOffice.Extensions { /// /// Extension methods for @@ -25,19 +24,6 @@ namespace Umbraco.Extensions return identityBuilder; } - /// - /// Adds a for the . - /// - /// The usermanager interface - /// The usermanager type - /// The current instance. - public static IdentityBuilder AddMembersUserManager(this IdentityBuilder identityBuilder) - where TUserManager : UserManager, TInterface - { - identityBuilder.Services.AddScoped(typeof(TInterface), typeof(TUserManager)); - return identityBuilder; - } - /// /// Adds a implementation for /// diff --git a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs index dd7eda895e..5733a1a634 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs @@ -1,7 +1,9 @@ using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Web.Caching; using SixLabors.ImageSharp.Web.Commands; @@ -9,6 +11,9 @@ using SixLabors.ImageSharp.Web.DependencyInjection; using SixLabors.ImageSharp.Web.Processors; using SixLabors.ImageSharp.Web.Providers; using Umbraco.Core.Configuration.Models; +using Umbraco.Infrastructure.Security; +using Umbraco.Web.Common.Extensions; +using Umbraco.Web.Common.Security; namespace Umbraco.Web.Common.DependencyInjection { @@ -55,6 +60,25 @@ namespace Umbraco.Web.Common.DependencyInjection return services; } + /// + /// Adds the services required for using Members Identity + /// + public static void AddMembersIdentity(this IServiceCollection services) => + services.BuildMembersIdentity() + .AddDefaultTokenProviders() + .AddUserStore() + .AddMembersUserManager(); + + + private static MembersIdentityBuilder BuildMembersIdentity(this IServiceCollection services) + { + // Services used by Umbraco members identity + services.TryAddScoped, UserValidator>(); + services.TryAddScoped, PasswordValidator>(); + services.TryAddScoped, PasswordHasher>(); + return new MembersIdentityBuilder(services); + } + private static void RemoveIntParamenterIfValueGreatherThen(IDictionary commands, string parameter, int maxValue) { if (commands.TryGetValue(parameter, out var command)) From a931f3c263a2fe040beec3b7ff66b7dce49e5524 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Sun, 14 Feb 2021 13:01:27 +0000 Subject: [PATCH 61/72] Renamed members manager method --- .../ServiceCollectionExtensions.cs | 2 +- .../Extensions/IdentityBuilderExtensions.cs | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs diff --git a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs index 5733a1a634..fa02478565 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs @@ -67,7 +67,7 @@ namespace Umbraco.Web.Common.DependencyInjection services.BuildMembersIdentity() .AddDefaultTokenProviders() .AddUserStore() - .AddMembersUserManager(); + .AddMembersManager(); private static MembersIdentityBuilder BuildMembersIdentity(this IServiceCollection services) diff --git a/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs new file mode 100644 index 0000000000..1a2f66ef0a --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Infrastructure.Security; + +namespace Umbraco.Web.Common.Extensions +{ + /// + /// Extension methods for + /// + public static class IdentityBuilderExtensions + { + /// + /// Adds a for the . + /// + /// The usermanager interface + /// The usermanager type + /// The current instance. + public static IdentityBuilder AddMembersManager(this IdentityBuilder identityBuilder) + where TUserManager : UserManager, TInterface + { + identityBuilder.Services.AddScoped(typeof(TInterface), typeof(TUserManager)); + return identityBuilder; + } + } +} From e74836cb488d69039592c855e81bca007d904716 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Thu, 18 Feb 2021 08:14:27 +0100 Subject: [PATCH 62/72] Moving classes to Infrastructure proj --- .../ModelsBuilder}/ApiVersion.cs | 9 +++------ .../ModelsBuilder}/Building/Builder.cs | 5 ++--- .../Building/ModelsGenerator.cs | 5 ++--- .../ModelsBuilder}/Building/PropertyModel.cs | 4 ++-- .../ModelsBuilder}/Building/TextBuilder.cs | 6 +++--- .../Building/TextHeaderWriter.cs | 4 ++-- .../ModelsBuilder}/Building/TypeModel.cs | 4 ++-- .../Building/TypeModelHasher.cs | 6 ++---- .../ImplementPropertyTypeAttribute.cs | 16 ++++++++++++++++ .../ModelsBuilder}/LiveModelsProvider.cs | 5 ++--- .../ModelsBuilderAssemblyAttribute.cs | 4 ++-- .../ModelsBuilder}/ModelsBuilderDashboard.cs | 2 +- .../ModelsBuilder}/ModelsGenerationError.cs | 2 +- .../ModelsBuilder}/OutOfDateModelsStatus.cs | 2 +- .../PublishedElementExtensions.cs | 4 ++-- .../ModelsBuilder}/PublishedModelUtility.cs | 4 ++-- .../ModelsBuilder}/RoslynCompiler.cs | 2 +- .../ModelsBuilder}/TypeExtensions.cs | 4 ++-- .../ModelsBuilder}/UmbracoServices.cs | 4 ++-- .../Umbraco.Infrastructure.csproj | 3 ++- .../ImplementPropertyTypeAttribute.cs | 19 ------------------- 21 files changed, 52 insertions(+), 62 deletions(-) rename src/{Umbraco.ModelsBuilder.Embedded => Umbraco.Infrastructure/ModelsBuilder}/ApiVersion.cs (81%) rename src/{Umbraco.ModelsBuilder.Embedded => Umbraco.Infrastructure/ModelsBuilder}/Building/Builder.cs (98%) rename src/{Umbraco.ModelsBuilder.Embedded => Umbraco.Infrastructure/ModelsBuilder}/Building/ModelsGenerator.cs (95%) rename src/{Umbraco.ModelsBuilder.Embedded => Umbraco.Infrastructure/ModelsBuilder}/Building/PropertyModel.cs (96%) rename src/{Umbraco.ModelsBuilder.Embedded => Umbraco.Infrastructure/ModelsBuilder}/Building/TextBuilder.cs (99%) rename src/{Umbraco.ModelsBuilder.Embedded => Umbraco.Infrastructure/ModelsBuilder}/Building/TextHeaderWriter.cs (92%) rename src/{Umbraco.ModelsBuilder.Embedded => Umbraco.Infrastructure/ModelsBuilder}/Building/TypeModel.cs (99%) rename src/{Umbraco.ModelsBuilder.Embedded => Umbraco.Infrastructure/ModelsBuilder}/Building/TypeModelHasher.cs (93%) create mode 100644 src/Umbraco.Infrastructure/ModelsBuilder/ImplementPropertyTypeAttribute.cs rename src/{Umbraco.ModelsBuilder.Embedded => Umbraco.Infrastructure/ModelsBuilder}/LiveModelsProvider.cs (97%) rename src/{Umbraco.ModelsBuilder.Embedded => Umbraco.Infrastructure/ModelsBuilder}/ModelsBuilderAssemblyAttribute.cs (93%) rename src/{Umbraco.ModelsBuilder.Embedded => Umbraco.Infrastructure/ModelsBuilder}/ModelsBuilderDashboard.cs (90%) rename src/{Umbraco.ModelsBuilder.Embedded => Umbraco.Infrastructure/ModelsBuilder}/ModelsGenerationError.cs (97%) rename src/{Umbraco.ModelsBuilder.Embedded => Umbraco.Infrastructure/ModelsBuilder}/OutOfDateModelsStatus.cs (98%) rename src/{Umbraco.ModelsBuilder.Embedded => Umbraco.Infrastructure/ModelsBuilder}/PublishedElementExtensions.cs (97%) rename src/{Umbraco.ModelsBuilder.Embedded => Umbraco.Infrastructure/ModelsBuilder}/PublishedModelUtility.cs (98%) rename src/{Umbraco.ModelsBuilder.Embedded => Umbraco.Infrastructure/ModelsBuilder}/RoslynCompiler.cs (98%) rename src/{Umbraco.ModelsBuilder.Embedded => Umbraco.Infrastructure/ModelsBuilder}/TypeExtensions.cs (94%) rename src/{Umbraco.ModelsBuilder.Embedded => Umbraco.Infrastructure/ModelsBuilder}/UmbracoServices.cs (99%) delete mode 100644 src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs diff --git a/src/Umbraco.ModelsBuilder.Embedded/ApiVersion.cs b/src/Umbraco.Infrastructure/ModelsBuilder/ApiVersion.cs similarity index 81% rename from src/Umbraco.ModelsBuilder.Embedded/ApiVersion.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/ApiVersion.cs index 22347edd60..aceb512dc4 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/ApiVersion.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/ApiVersion.cs @@ -1,8 +1,8 @@ -using System; +using System; using System.Reflection; using Semver; -namespace Umbraco.ModelsBuilder.Embedded +namespace Umbraco.Infrastructure.ModelsBuilder { /// /// Manages API version handshake between client and server. @@ -14,10 +14,7 @@ namespace Umbraco.ModelsBuilder.Embedded /// /// The currently executing version. /// - internal ApiVersion(SemVersion executingVersion) - { - Version = executingVersion ?? throw new ArgumentNullException(nameof(executingVersion)); - } + internal ApiVersion(SemVersion executingVersion) => Version = executingVersion ?? throw new ArgumentNullException(nameof(executingVersion)); private static SemVersion CurrentAssemblyVersion => SemVersion.Parse(Assembly.GetExecutingAssembly().GetCustomAttribute().InformationalVersion); diff --git a/src/Umbraco.ModelsBuilder.Embedded/Building/Builder.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/Builder.cs similarity index 98% rename from src/Umbraco.ModelsBuilder.Embedded/Building/Builder.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/Building/Builder.cs index aa7ab40ba5..ebde20fbbe 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Building/Builder.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/Builder.cs @@ -1,11 +1,11 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; -namespace Umbraco.ModelsBuilder.Embedded.Building +namespace Umbraco.Infrastructure.ModelsBuilder.Building { // NOTE // The idea was to have different types of builder, because I wanted to experiment with @@ -30,7 +30,6 @@ namespace Umbraco.ModelsBuilder.Embedded.Building "System.Linq.Expressions", "Umbraco.Core.Models.PublishedContent", "Umbraco.Web.PublishedCache", - "Umbraco.ModelsBuilder.Embedded", "Umbraco.Core" }; diff --git a/src/Umbraco.ModelsBuilder.Embedded/Building/ModelsGenerator.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/ModelsGenerator.cs similarity index 95% rename from src/Umbraco.ModelsBuilder.Embedded/Building/ModelsGenerator.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/Building/ModelsGenerator.cs index 9431b0141a..63bda2689c 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Building/ModelsGenerator.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/ModelsGenerator.cs @@ -4,9 +4,8 @@ using Microsoft.Extensions.Options; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Hosting; -using Umbraco.Core.IO; -namespace Umbraco.ModelsBuilder.Embedded.Building +namespace Umbraco.Infrastructure.ModelsBuilder.Building { public class ModelsGenerator { @@ -23,7 +22,7 @@ namespace Umbraco.ModelsBuilder.Embedded.Building _hostingEnvironment = hostingEnvironment; } - internal void GenerateModels() + public void GenerateModels() { var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); if (!Directory.Exists(modelsDirectory)) diff --git a/src/Umbraco.ModelsBuilder.Embedded/Building/PropertyModel.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/PropertyModel.cs similarity index 96% rename from src/Umbraco.ModelsBuilder.Embedded/Building/PropertyModel.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/Building/PropertyModel.cs index af5445b175..de0bc8f395 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Building/PropertyModel.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/PropertyModel.cs @@ -1,7 +1,7 @@ -using System; +using System; using System.Collections.Generic; -namespace Umbraco.ModelsBuilder.Embedded.Building +namespace Umbraco.Infrastructure.ModelsBuilder.Building { /// /// Represents a model property. diff --git a/src/Umbraco.ModelsBuilder.Embedded/Building/TextBuilder.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs similarity index 99% rename from src/Umbraco.ModelsBuilder.Embedded/Building/TextBuilder.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs index 8328afb822..28e71d7f4b 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Building/TextBuilder.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs @@ -1,16 +1,16 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using Umbraco.Core.Configuration.Models; -namespace Umbraco.ModelsBuilder.Embedded.Building +namespace Umbraco.Infrastructure.ModelsBuilder.Building { /// /// Implements a builder that works by writing text. /// - internal class TextBuilder : Builder + public class TextBuilder : Builder { /// /// Initializes a new instance of the class with a list of models to generate diff --git a/src/Umbraco.ModelsBuilder.Embedded/Building/TextHeaderWriter.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextHeaderWriter.cs similarity index 92% rename from src/Umbraco.ModelsBuilder.Embedded/Building/TextHeaderWriter.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/Building/TextHeaderWriter.cs index 0ffad1c5bc..4c9c81e7aa 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Building/TextHeaderWriter.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextHeaderWriter.cs @@ -1,6 +1,6 @@ -using System.Text; +using System.Text; -namespace Umbraco.ModelsBuilder.Embedded.Building +namespace Umbraco.Infrastructure.ModelsBuilder.Building { internal static class TextHeaderWriter { diff --git a/src/Umbraco.ModelsBuilder.Embedded/Building/TypeModel.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModel.cs similarity index 99% rename from src/Umbraco.ModelsBuilder.Embedded/Building/TypeModel.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModel.cs index 95356cf3ff..7ded306f60 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Building/TypeModel.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModel.cs @@ -1,9 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core.Models.PublishedContent; -namespace Umbraco.ModelsBuilder.Embedded.Building +namespace Umbraco.Infrastructure.ModelsBuilder.Building { /// /// Represents a model. diff --git a/src/Umbraco.ModelsBuilder.Embedded/Building/TypeModelHasher.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModelHasher.cs similarity index 93% rename from src/Umbraco.ModelsBuilder.Embedded/Building/TypeModelHasher.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModelHasher.cs index c5b053ca07..ab8043a72b 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Building/TypeModelHasher.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModelHasher.cs @@ -1,13 +1,11 @@ -using System; using System.Collections.Generic; using System.Linq; -using System.Security.Cryptography; using System.Text; using Umbraco.Core; -namespace Umbraco.ModelsBuilder.Embedded.Building +namespace Umbraco.Infrastructure.ModelsBuilder.Building { - internal class TypeModelHasher + public class TypeModelHasher { public static string Hash(IEnumerable typeModels) { diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/ImplementPropertyTypeAttribute.cs b/src/Umbraco.Infrastructure/ModelsBuilder/ImplementPropertyTypeAttribute.cs new file mode 100644 index 0000000000..6a0d890cce --- /dev/null +++ b/src/Umbraco.Infrastructure/ModelsBuilder/ImplementPropertyTypeAttribute.cs @@ -0,0 +1,16 @@ +using System; + +namespace Umbraco.Infrastructure.ModelsBuilder +{ + /// + /// Indicates that a property implements a given property alias. + /// + /// And therefore it should not be generated. + [AttributeUsage(AttributeTargets.Property /*, AllowMultiple = false, Inherited = false*/)] + public class ImplementPropertyTypeAttribute : Attribute + { + public ImplementPropertyTypeAttribute(string alias) => Alias = alias; + + public string Alias { get; } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs b/src/Umbraco.Infrastructure/ModelsBuilder/LiveModelsProvider.cs similarity index 97% rename from src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/LiveModelsProvider.cs index eafc006c26..eea958e36f 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/LiveModelsProvider.cs @@ -6,11 +6,10 @@ using Umbraco.Configuration; using Umbraco.Core; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Events; -using Umbraco.Extensions; -using Umbraco.ModelsBuilder.Embedded.Building; +using Umbraco.Infrastructure.ModelsBuilder.Building; using Umbraco.Web.Cache; -namespace Umbraco.ModelsBuilder.Embedded +namespace Umbraco.Infrastructure.ModelsBuilder { // supports LiveAppData - but not PureLive public sealed class LiveModelsProvider : INotificationHandler, INotificationHandler diff --git a/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderAssemblyAttribute.cs b/src/Umbraco.Infrastructure/ModelsBuilder/ModelsBuilderAssemblyAttribute.cs similarity index 93% rename from src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderAssemblyAttribute.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/ModelsBuilderAssemblyAttribute.cs index 7570c0b5b2..56179f37ac 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderAssemblyAttribute.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/ModelsBuilderAssemblyAttribute.cs @@ -1,6 +1,6 @@ -using System; +using System; -namespace Umbraco.ModelsBuilder.Embedded +namespace Umbraco.Infrastructure.ModelsBuilder { /// /// Indicates that an Assembly is a Models Builder assembly. diff --git a/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderDashboard.cs b/src/Umbraco.Infrastructure/ModelsBuilder/ModelsBuilderDashboard.cs similarity index 90% rename from src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderDashboard.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/ModelsBuilderDashboard.cs index 867b22d14b..9c2d2c03b5 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderDashboard.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/ModelsBuilderDashboard.cs @@ -2,7 +2,7 @@ using System; using Umbraco.Core.Composing; using Umbraco.Core.Dashboards; -namespace Umbraco.ModelsBuilder.Embedded +namespace Umbraco.Infrastructure.ModelsBuilder { [Weight(40)] public class ModelsBuilderDashboard : IDashboard diff --git a/src/Umbraco.ModelsBuilder.Embedded/ModelsGenerationError.cs b/src/Umbraco.Infrastructure/ModelsBuilder/ModelsGenerationError.cs similarity index 97% rename from src/Umbraco.ModelsBuilder.Embedded/ModelsGenerationError.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/ModelsGenerationError.cs index 25f48a19cc..c506c49049 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/ModelsGenerationError.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/ModelsGenerationError.cs @@ -6,7 +6,7 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Hosting; -namespace Umbraco.ModelsBuilder.Embedded +namespace Umbraco.Infrastructure.ModelsBuilder { public sealed class ModelsGenerationError { diff --git a/src/Umbraco.ModelsBuilder.Embedded/OutOfDateModelsStatus.cs b/src/Umbraco.Infrastructure/ModelsBuilder/OutOfDateModelsStatus.cs similarity index 98% rename from src/Umbraco.ModelsBuilder.Embedded/OutOfDateModelsStatus.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/OutOfDateModelsStatus.cs index 83f105a486..d1caca2d46 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/OutOfDateModelsStatus.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/OutOfDateModelsStatus.cs @@ -6,7 +6,7 @@ using Umbraco.Core.Events; using Umbraco.Core.Hosting; using Umbraco.Web.Cache; -namespace Umbraco.ModelsBuilder.Embedded +namespace Umbraco.Infrastructure.ModelsBuilder { /// /// Used to track if ModelsBuilder models are out of date/stale diff --git a/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs b/src/Umbraco.Infrastructure/ModelsBuilder/PublishedElementExtensions.cs similarity index 97% rename from src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/PublishedElementExtensions.cs index 0611d466dc..2a767b718b 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/PublishedElementExtensions.cs @@ -1,8 +1,8 @@ -using System; +using System; using System.Linq.Expressions; using System.Reflection; using Umbraco.Core.Models.PublishedContent; -using Umbraco.ModelsBuilder.Embedded; +using Umbraco.Infrastructure.ModelsBuilder; // same namespace as original Umbraco.Web PublishedElementExtensions // ReSharper disable once CheckNamespace diff --git a/src/Umbraco.ModelsBuilder.Embedded/PublishedModelUtility.cs b/src/Umbraco.Infrastructure/ModelsBuilder/PublishedModelUtility.cs similarity index 98% rename from src/Umbraco.ModelsBuilder.Embedded/PublishedModelUtility.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/PublishedModelUtility.cs index fd1d5128a0..6638544d9c 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/PublishedModelUtility.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/PublishedModelUtility.cs @@ -1,10 +1,10 @@ -using System; +using System; using System.Linq; using System.Linq.Expressions; using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.PublishedCache; -namespace Umbraco.ModelsBuilder.Embedded +namespace Umbraco.Infrastructure.ModelsBuilder { /// /// This is called from within the generated model classes diff --git a/src/Umbraco.ModelsBuilder.Embedded/RoslynCompiler.cs b/src/Umbraco.Infrastructure/ModelsBuilder/RoslynCompiler.cs similarity index 98% rename from src/Umbraco.ModelsBuilder.Embedded/RoslynCompiler.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/RoslynCompiler.cs index 37aeb75b35..e2e7affffa 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/RoslynCompiler.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/RoslynCompiler.cs @@ -7,7 +7,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Text; -namespace Umbraco.ModelsBuilder.Embedded +namespace Umbraco.Infrastructure.ModelsBuilder { public class RoslynCompiler { diff --git a/src/Umbraco.ModelsBuilder.Embedded/TypeExtensions.cs b/src/Umbraco.Infrastructure/ModelsBuilder/TypeExtensions.cs similarity index 94% rename from src/Umbraco.ModelsBuilder.Embedded/TypeExtensions.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/TypeExtensions.cs index 1f270a80a6..1a29931a1e 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/TypeExtensions.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/TypeExtensions.cs @@ -1,6 +1,6 @@ -using System; +using System; -namespace Umbraco.ModelsBuilder.Embedded +namespace Umbraco.Infrastructure.ModelsBuilder { internal static class TypeExtensions { diff --git a/src/Umbraco.ModelsBuilder.Embedded/UmbracoServices.cs b/src/Umbraco.Infrastructure/ModelsBuilder/UmbracoServices.cs similarity index 99% rename from src/Umbraco.ModelsBuilder.Embedded/UmbracoServices.cs rename to src/Umbraco.Infrastructure/ModelsBuilder/UmbracoServices.cs index 86954a8a85..be59e7aab8 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/UmbracoServices.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/UmbracoServices.cs @@ -7,9 +7,9 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Services; using Umbraco.Core.Strings; -using Umbraco.ModelsBuilder.Embedded.Building; +using Umbraco.Infrastructure.ModelsBuilder.Building; -namespace Umbraco.ModelsBuilder.Embedded +namespace Umbraco.Infrastructure.ModelsBuilder { public sealed class UmbracoServices diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index f295805a55..7eac57df29 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 @@ -12,6 +12,7 @@ + diff --git a/src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs b/src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs deleted file mode 100644 index 6f52a7faa9..0000000000 --- a/src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace Umbraco.ModelsBuilder.Embedded -{ - /// - /// Indicates that a property implements a given property alias. - /// - /// And therefore it should not be generated. - [AttributeUsage(AttributeTargets.Property , AllowMultiple = false, Inherited = false)] - public class ImplementPropertyTypeAttribute : Attribute - { - public ImplementPropertyTypeAttribute(string alias) - { - Alias = alias; - } - - public string Alias { get; private set; } - } -} From 570d19f298a9e9ce296090fac683c0acf35ad232 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Thu, 18 Feb 2021 08:21:48 +0100 Subject: [PATCH 63/72] Migrating classes to Web.BackOffice proj --- .../ContentTypeModelValidator.cs | 4 ++-- .../ContentTypeModelValidatorBase.cs | 2 +- .../ModelsBuilder}/DashboardReport.cs | 3 ++- ...DisableModelsBuilderNotificationHandler.cs | 8 ++----- .../ModelsBuilder}/MediaTypeModelValidator.cs | 4 ++-- .../MemberTypeModelValidator.cs | 4 ++-- .../ModelsBuilderDashboardController.cs | 5 +++-- .../ModelsBuilder/UmbracoBuilderExtensions.cs | 21 +++++++++++++++++++ 8 files changed, 35 insertions(+), 16 deletions(-) rename src/{Umbraco.ModelsBuilder.Embedded/BackOffice => Umbraco.Web.BackOffice/ModelsBuilder}/ContentTypeModelValidator.cs (86%) rename src/{Umbraco.ModelsBuilder.Embedded/BackOffice => Umbraco.Web.BackOffice/ModelsBuilder}/ContentTypeModelValidatorBase.cs (98%) rename src/{Umbraco.ModelsBuilder.Embedded/BackOffice => Umbraco.Web.BackOffice/ModelsBuilder}/DashboardReport.cs (97%) rename src/{Umbraco.ModelsBuilder.Embedded => Umbraco.Web.BackOffice/ModelsBuilder}/DisableModelsBuilderNotificationHandler.cs (83%) rename src/{Umbraco.ModelsBuilder.Embedded/BackOffice => Umbraco.Web.BackOffice/ModelsBuilder}/MediaTypeModelValidator.cs (86%) rename src/{Umbraco.ModelsBuilder.Embedded/BackOffice => Umbraco.Web.BackOffice/ModelsBuilder}/MemberTypeModelValidator.cs (86%) rename src/{Umbraco.ModelsBuilder.Embedded/BackOffice => Umbraco.Web.BackOffice/ModelsBuilder}/ModelsBuilderDashboardController.cs (97%) create mode 100644 src/Umbraco.Web.BackOffice/ModelsBuilder/UmbracoBuilderExtensions.cs diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidator.cs b/src/Umbraco.Web.BackOffice/ModelsBuilder/ContentTypeModelValidator.cs similarity index 86% rename from src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidator.cs rename to src/Umbraco.Web.BackOffice/ModelsBuilder/ContentTypeModelValidator.cs index 023911d518..9906472d9a 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidator.cs +++ b/src/Umbraco.Web.BackOffice/ModelsBuilder/ContentTypeModelValidator.cs @@ -1,8 +1,8 @@ -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options; using Umbraco.Core.Configuration.Models; using Umbraco.Web.Models.ContentEditing; -namespace Umbraco.ModelsBuilder.Embedded.BackOffice +namespace Umbraco.Web.BackOffice.ModelsBuilder { /// /// Used to validate the aliases for the content type when MB is enabled to ensure that diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidatorBase.cs b/src/Umbraco.Web.BackOffice/ModelsBuilder/ContentTypeModelValidatorBase.cs similarity index 98% rename from src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidatorBase.cs rename to src/Umbraco.Web.BackOffice/ModelsBuilder/ContentTypeModelValidatorBase.cs index c34f4516e4..754184079e 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidatorBase.cs +++ b/src/Umbraco.Web.BackOffice/ModelsBuilder/ContentTypeModelValidatorBase.cs @@ -10,7 +10,7 @@ using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.Editors; using Umbraco.Web.Models.ContentEditing; -namespace Umbraco.ModelsBuilder.Embedded.BackOffice +namespace Umbraco.Web.BackOffice.ModelsBuilder { public abstract class ContentTypeModelValidatorBase : EditorValidator where TModel : ContentTypeSave diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/DashboardReport.cs b/src/Umbraco.Web.BackOffice/ModelsBuilder/DashboardReport.cs similarity index 97% rename from src/Umbraco.ModelsBuilder.Embedded/BackOffice/DashboardReport.cs rename to src/Umbraco.Web.BackOffice/ModelsBuilder/DashboardReport.cs index 6425673916..9aef059095 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/DashboardReport.cs +++ b/src/Umbraco.Web.BackOffice/ModelsBuilder/DashboardReport.cs @@ -4,8 +4,9 @@ using Umbraco.Configuration; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; +using Umbraco.Infrastructure.ModelsBuilder; -namespace Umbraco.ModelsBuilder.Embedded.BackOffice +namespace Umbraco.Web.BackOffice.ModelsBuilder { internal class DashboardReport { diff --git a/src/Umbraco.ModelsBuilder.Embedded/DisableModelsBuilderNotificationHandler.cs b/src/Umbraco.Web.BackOffice/ModelsBuilder/DisableModelsBuilderNotificationHandler.cs similarity index 83% rename from src/Umbraco.ModelsBuilder.Embedded/DisableModelsBuilderNotificationHandler.cs rename to src/Umbraco.Web.BackOffice/ModelsBuilder/DisableModelsBuilderNotificationHandler.cs index b455bbbf61..0cd0742708 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/DisableModelsBuilderNotificationHandler.cs +++ b/src/Umbraco.Web.BackOffice/ModelsBuilder/DisableModelsBuilderNotificationHandler.cs @@ -1,9 +1,7 @@ using Umbraco.Core.Events; -using Umbraco.ModelsBuilder.Embedded.BackOffice; -using Umbraco.ModelsBuilder.Embedded.DependencyInjection; using Umbraco.Web.Features; -namespace Umbraco.ModelsBuilder.Embedded +namespace Umbraco.Web.BackOffice.ModelsBuilder { /// /// Used in conjunction with @@ -17,10 +15,8 @@ namespace Umbraco.ModelsBuilder.Embedded /// /// Handles the notification to disable MB controller features /// - public void Handle(UmbracoApplicationStarting notification) - { + public void Handle(UmbracoApplicationStarting notification) => // disable the embedded dashboard controller _features.Disabled.Controllers.Add(); - } } } diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/MediaTypeModelValidator.cs b/src/Umbraco.Web.BackOffice/ModelsBuilder/MediaTypeModelValidator.cs similarity index 86% rename from src/Umbraco.ModelsBuilder.Embedded/BackOffice/MediaTypeModelValidator.cs rename to src/Umbraco.Web.BackOffice/ModelsBuilder/MediaTypeModelValidator.cs index 4ccdc1b362..f2b3a5344d 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/MediaTypeModelValidator.cs +++ b/src/Umbraco.Web.BackOffice/ModelsBuilder/MediaTypeModelValidator.cs @@ -1,8 +1,8 @@ -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options; using Umbraco.Core.Configuration.Models; using Umbraco.Web.Models.ContentEditing; -namespace Umbraco.ModelsBuilder.Embedded.BackOffice +namespace Umbraco.Web.BackOffice.ModelsBuilder { /// /// Used to validate the aliases for the content type when MB is enabled to ensure that diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/MemberTypeModelValidator.cs b/src/Umbraco.Web.BackOffice/ModelsBuilder/MemberTypeModelValidator.cs similarity index 86% rename from src/Umbraco.ModelsBuilder.Embedded/BackOffice/MemberTypeModelValidator.cs rename to src/Umbraco.Web.BackOffice/ModelsBuilder/MemberTypeModelValidator.cs index 9a735631ff..1c5a0e5741 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/MemberTypeModelValidator.cs +++ b/src/Umbraco.Web.BackOffice/ModelsBuilder/MemberTypeModelValidator.cs @@ -1,8 +1,8 @@ -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options; using Umbraco.Core.Configuration.Models; using Umbraco.Web.Models.ContentEditing; -namespace Umbraco.ModelsBuilder.Embedded.BackOffice +namespace Umbraco.Web.BackOffice.ModelsBuilder { /// /// Used to validate the aliases for the content type when MB is enabled to ensure that diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs b/src/Umbraco.Web.BackOffice/ModelsBuilder/ModelsBuilderDashboardController.cs similarity index 97% rename from src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs rename to src/Umbraco.Web.BackOffice/ModelsBuilder/ModelsBuilderDashboardController.cs index f242854b3f..f913a43c37 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs +++ b/src/Umbraco.Web.BackOffice/ModelsBuilder/ModelsBuilderDashboardController.cs @@ -6,11 +6,12 @@ using Microsoft.Extensions.Options; using Umbraco.Configuration; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; -using Umbraco.ModelsBuilder.Embedded.Building; +using Umbraco.Infrastructure.ModelsBuilder; +using Umbraco.Infrastructure.ModelsBuilder.Building; using Umbraco.Web.BackOffice.Controllers; using Umbraco.Web.Common.Authorization; -namespace Umbraco.ModelsBuilder.Embedded.BackOffice +namespace Umbraco.Web.BackOffice.ModelsBuilder { /// /// API controller for use in the Umbraco back office with Angular resources diff --git a/src/Umbraco.Web.BackOffice/ModelsBuilder/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/ModelsBuilder/UmbracoBuilderExtensions.cs new file mode 100644 index 0000000000..d5c89a9938 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/ModelsBuilder/UmbracoBuilderExtensions.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Core.DependencyInjection; + +namespace Umbraco.Web.BackOffice.ModelsBuilder +{ + /// + /// Extension methods for for the common Umbraco functionality + /// + public static class UmbracoBuilderExtensions + { + /// + /// Can be called if using an external models builder to remove the embedded models builder controller features + /// + public static IUmbracoBuilder DisableModelsBuilderControllers(this IUmbracoBuilder builder) + { + builder.Services.AddSingleton(); + return builder; + } + + } +} From 74a67bf8bc5a0216f8b97198efce852c819cf27f Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Thu, 18 Feb 2021 08:26:08 +0100 Subject: [PATCH 64/72] Migrating classes to Web.Common proj --- .../ModelsBuilder}/PureLiveModelFactory.cs | 7 +++---- .../ModelsBuilder}/RefreshingRazorViewEngine.cs | 2 +- .../ModelsBuilder}/UmbracoAssemblyLoadContext.cs | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) rename src/{Umbraco.ModelsBuilder.Embedded => Umbraco.Web.Common/ModelsBuilder}/PureLiveModelFactory.cs (99%) rename src/{Umbraco.ModelsBuilder.Embedded => Umbraco.Web.Common/ModelsBuilder}/RefreshingRazorViewEngine.cs (99%) rename src/{Umbraco.ModelsBuilder.Embedded => Umbraco.Web.Common/ModelsBuilder}/UmbracoAssemblyLoadContext.cs (94%) diff --git a/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs b/src/Umbraco.Web.Common/ModelsBuilder/PureLiveModelFactory.cs similarity index 99% rename from src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs rename to src/Umbraco.Web.Common/ModelsBuilder/PureLiveModelFactory.cs index 41a0ac86f9..a3fc4cfdb6 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs +++ b/src/Umbraco.Web.Common/ModelsBuilder/PureLiveModelFactory.cs @@ -10,7 +10,6 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading; using Microsoft.AspNetCore.Mvc.ApplicationParts; -using Microsoft.CodeAnalysis; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Core; @@ -19,10 +18,10 @@ using Umbraco.Core.Configuration.Models; using Umbraco.Core.Hosting; using Umbraco.Core.Logging; using Umbraco.Core.Models.PublishedContent; -using Umbraco.ModelsBuilder.Embedded.Building; -using File = System.IO.File; +using Umbraco.Infrastructure.ModelsBuilder; +using Umbraco.Infrastructure.ModelsBuilder.Building; -namespace Umbraco.ModelsBuilder.Embedded +namespace Umbraco.Web.Common.ModelsBuilder { internal class PureLiveModelFactory : ILivePublishedModelFactory, IRegisteredObject { diff --git a/src/Umbraco.ModelsBuilder.Embedded/RefreshingRazorViewEngine.cs b/src/Umbraco.Web.Common/ModelsBuilder/RefreshingRazorViewEngine.cs similarity index 99% rename from src/Umbraco.ModelsBuilder.Embedded/RefreshingRazorViewEngine.cs rename to src/Umbraco.Web.Common/ModelsBuilder/RefreshingRazorViewEngine.cs index ad82d1d7b3..82011ad31d 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/RefreshingRazorViewEngine.cs +++ b/src/Umbraco.Web.Common/ModelsBuilder/RefreshingRazorViewEngine.cs @@ -60,7 +60,7 @@ using Microsoft.AspNetCore.Mvc.ViewEngines; * graph includes all of the above mentioned services, all the way up to the RazorProjectEngine and it's LazyMetadataReferenceFeature. */ -namespace Umbraco.ModelsBuilder.Embedded +namespace Umbraco.Web.Common.ModelsBuilder { /// /// Custom that wraps aspnetcore's default implementation diff --git a/src/Umbraco.ModelsBuilder.Embedded/UmbracoAssemblyLoadContext.cs b/src/Umbraco.Web.Common/ModelsBuilder/UmbracoAssemblyLoadContext.cs similarity index 94% rename from src/Umbraco.ModelsBuilder.Embedded/UmbracoAssemblyLoadContext.cs rename to src/Umbraco.Web.Common/ModelsBuilder/UmbracoAssemblyLoadContext.cs index d89714adbe..76a774889b 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/UmbracoAssemblyLoadContext.cs +++ b/src/Umbraco.Web.Common/ModelsBuilder/UmbracoAssemblyLoadContext.cs @@ -1,7 +1,7 @@ using System.Reflection; using System.Runtime.Loader; -namespace Umbraco.ModelsBuilder.Embedded +namespace Umbraco.Web.Common.ModelsBuilder { internal class UmbracoAssemblyLoadContext : AssemblyLoadContext { From 4bccb995ecedb701e7072cf2d78c4421bc1637a5 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Thu, 18 Feb 2021 08:27:35 +0100 Subject: [PATCH 65/72] Introducing IModelsBuilderDashboardProvider --- .../ModelsBuilderDashboardProvider.cs | 20 +++++++++++++++++++ .../ModelsBuilder/UmbracoBuilderExtensions.cs | 2 ++ .../UmbracoBuilderExtensions.cs | 18 ++++------------- .../IModelsBuilderDashboardProvider.cs | 14 +++++++++++++ .../ModelsBuilderNotificationHandler.cs | 14 ++++++------- .../NoopModelsBuilderDashboardProvider.cs | 13 ++++++++++++ 6 files changed, 59 insertions(+), 22 deletions(-) create mode 100644 src/Umbraco.Web.BackOffice/ModelsBuilder/ModelsBuilderDashboardProvider.cs rename src/{Umbraco.ModelsBuilder.Embedded => Umbraco.Web.Common/ModelsBuilder}/DependencyInjection/UmbracoBuilderExtensions.cs (93%) create mode 100644 src/Umbraco.Web.Common/ModelsBuilder/IModelsBuilderDashboardProvider.cs rename src/{Umbraco.ModelsBuilder.Embedded => Umbraco.Web.Common/ModelsBuilder}/ModelsBuilderNotificationHandler.cs (94%) create mode 100644 src/Umbraco.Web.Common/ModelsBuilder/NoopModelsBuilderDashboardProvider.cs diff --git a/src/Umbraco.Web.BackOffice/ModelsBuilder/ModelsBuilderDashboardProvider.cs b/src/Umbraco.Web.BackOffice/ModelsBuilder/ModelsBuilderDashboardProvider.cs new file mode 100644 index 0000000000..f84982a04c --- /dev/null +++ b/src/Umbraco.Web.BackOffice/ModelsBuilder/ModelsBuilderDashboardProvider.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Routing; +using Umbraco.Extensions; +using Umbraco.Web.Common.ModelsBuilder; + +namespace Umbraco.Web.BackOffice.ModelsBuilder +{ + public class ModelsBuilderDashboardProvider: IModelsBuilderDashboardProvider + { + private readonly LinkGenerator _linkGenerator; + + public ModelsBuilderDashboardProvider(LinkGenerator linkGenerator) + { + _linkGenerator = linkGenerator; + } + + public string GetUrl() => + _linkGenerator.GetUmbracoApiServiceBaseUrl(controller => + controller.BuildModels()); + } +} diff --git a/src/Umbraco.Web.BackOffice/ModelsBuilder/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/ModelsBuilder/UmbracoBuilderExtensions.cs index d5c89a9938..7c35d1b845 100644 --- a/src/Umbraco.Web.BackOffice/ModelsBuilder/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/ModelsBuilder/UmbracoBuilderExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Core.DependencyInjection; +using Umbraco.Web.Common.ModelsBuilder; namespace Umbraco.Web.BackOffice.ModelsBuilder { @@ -14,6 +15,7 @@ namespace Umbraco.Web.BackOffice.ModelsBuilder public static IUmbracoBuilder DisableModelsBuilderControllers(this IUmbracoBuilder builder) { builder.Services.AddSingleton(); + builder.Services.AddUnique(); return builder; } diff --git a/src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/ModelsBuilder/DependencyInjection/UmbracoBuilderExtensions.cs similarity index 93% rename from src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/UmbracoBuilderExtensions.cs rename to src/Umbraco.Web.Common/ModelsBuilder/DependencyInjection/UmbracoBuilderExtensions.cs index 852cde55fc..068518da64 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/ModelsBuilder/DependencyInjection/UmbracoBuilderExtensions.cs @@ -1,17 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; -using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; using Umbraco.Core.DependencyInjection; using Umbraco.Core.Events; using Umbraco.Core.Models.PublishedContent; -using Umbraco.ModelsBuilder.Embedded.Building; +using Umbraco.Infrastructure.ModelsBuilder; +using Umbraco.Infrastructure.ModelsBuilder.Building; using Umbraco.Web.Common.ModelBinders; using Umbraco.Web.WebAssets; @@ -73,7 +70,7 @@ using Umbraco.Web.WebAssets; * graph includes all of the above mentioned services, all the way up to the RazorProjectEngine and it's LazyMetadataReferenceFeature. */ -namespace Umbraco.ModelsBuilder.Embedded.DependencyInjection +namespace Umbraco.Web.Common.ModelsBuilder.DependencyInjection { /// /// Extension methods for for the common Umbraco functionality @@ -126,15 +123,8 @@ namespace Umbraco.ModelsBuilder.Embedded.DependencyInjection } }); - return builder; - } + builder.Services.AddUnique(); - /// - /// Can be called if using an external models builder to remove the embedded models builder controller features - /// - public static IUmbracoBuilder DisableModelsBuilderControllers(this IUmbracoBuilder builder) - { - builder.Services.AddSingleton(); return builder; } diff --git a/src/Umbraco.Web.Common/ModelsBuilder/IModelsBuilderDashboardProvider.cs b/src/Umbraco.Web.Common/ModelsBuilder/IModelsBuilderDashboardProvider.cs new file mode 100644 index 0000000000..47af1d2a94 --- /dev/null +++ b/src/Umbraco.Web.Common/ModelsBuilder/IModelsBuilderDashboardProvider.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Web.Routing; + +namespace Umbraco.Web.Common.ModelsBuilder +{ + public interface IModelsBuilderDashboardProvider + { + string GetUrl(); + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderNotificationHandler.cs b/src/Umbraco.Web.Common/ModelsBuilder/ModelsBuilderNotificationHandler.cs similarity index 94% rename from src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderNotificationHandler.cs rename to src/Umbraco.Web.Common/ModelsBuilder/ModelsBuilderNotificationHandler.cs index 0d6d1cc668..d612b9040c 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderNotificationHandler.cs +++ b/src/Umbraco.Web.Common/ModelsBuilder/ModelsBuilderNotificationHandler.cs @@ -11,12 +11,11 @@ using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; using Umbraco.Core.Strings; -using Umbraco.Extensions; -using Umbraco.ModelsBuilder.Embedded.BackOffice; +using Umbraco.Infrastructure.ModelsBuilder; using Umbraco.Web.Common.ModelBinders; using Umbraco.Web.WebAssets; -namespace Umbraco.ModelsBuilder.Embedded +namespace Umbraco.Web.Common.ModelsBuilder { /// @@ -26,17 +25,16 @@ namespace Umbraco.ModelsBuilder.Embedded { private readonly ModelsBuilderSettings _config; private readonly IShortStringHelper _shortStringHelper; - private readonly LinkGenerator _linkGenerator; + private readonly IModelsBuilderDashboardProvider _modelsBuilderDashboardProvider; public ModelsBuilderNotificationHandler( IOptions config, IShortStringHelper shortStringHelper, - LinkGenerator linkGenerator) + IModelsBuilderDashboardProvider modelsBuilderDashboardProvider) { _config = config.Value; _shortStringHelper = shortStringHelper; - _shortStringHelper = shortStringHelper; - _linkGenerator = linkGenerator; + _modelsBuilderDashboardProvider = modelsBuilderDashboardProvider; } /// @@ -85,7 +83,7 @@ namespace Umbraco.ModelsBuilder.Embedded throw new ArgumentException("Invalid umbracoPlugins"); } - umbracoUrls["modelsBuilderBaseUrl"] = _linkGenerator.GetUmbracoApiServiceBaseUrl(controller => controller.BuildModels()); + umbracoUrls["modelsBuilderBaseUrl"] = _modelsBuilderDashboardProvider.GetUrl(); umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings(); } diff --git a/src/Umbraco.Web.Common/ModelsBuilder/NoopModelsBuilderDashboardProvider.cs b/src/Umbraco.Web.Common/ModelsBuilder/NoopModelsBuilderDashboardProvider.cs new file mode 100644 index 0000000000..7c5a0daabf --- /dev/null +++ b/src/Umbraco.Web.Common/ModelsBuilder/NoopModelsBuilderDashboardProvider.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Web.Common.ModelsBuilder +{ + public class NoopModelsBuilderDashboardProvider: IModelsBuilderDashboardProvider + { + public string GetUrl() => string.Empty; + } +} From a2cfd277ce32c64adc67628235bbda24b98ece1a Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Thu, 18 Feb 2021 08:33:49 +0100 Subject: [PATCH 66/72] Cleanup --- build/NuSpecs/UmbracoCms.Web.nuspec | 3 -- build/build.ps1 | 2 +- .../DefaultUmbracoAssemblyProvider.cs | 3 +- src/Umbraco.Core/Properties/AssemblyInfo.cs | 1 - .../Umbraco.ModelsBuilder.Embedded.csproj | 37 ------------------- .../BuilderTests.cs | 6 +-- .../UmbracoApplicationTests.cs | 4 +- .../Umbraco.Tests.UnitTests.csproj | 1 - src/Umbraco.Web.UI.NetCore/Startup.cs | 1 - .../Umbraco.Web.UI.NetCore.csproj | 1 - .../UmbracoBuilderExtensions.cs | 2 +- .../Umbraco.Web.Website.csproj | 1 - src/Umbraco.Web/Properties/AssemblyInfo.cs | 1 - src/umbraco.sln | 24 +++++------- 14 files changed, 16 insertions(+), 71 deletions(-) delete mode 100644 src/Umbraco.ModelsBuilder.Embedded/Umbraco.ModelsBuilder.Embedded.csproj diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec index 1d136daf95..92cb0f065e 100644 --- a/build/NuSpecs/UmbracoCms.Web.nuspec +++ b/build/NuSpecs/UmbracoCms.Web.nuspec @@ -43,18 +43,15 @@ - - - diff --git a/build/build.ps1 b/build/build.ps1 index 07d856d075..58d56fcdfe 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -478,7 +478,7 @@ { $this.VerifyNuGetConsistency( ("UmbracoCms", "UmbracoCms.Core", "UmbracoCms.Web"), - ("Umbraco.Core", "Umbraco.Infrastructure", "Umbraco.Web.UI.NetCore", "Umbraco.Examine.Lucene", "Umbraco.PublishedCache.NuCache", "Umbraco.Web.Common", "Umbraco.Web.Website", "Umbraco.Web.BackOffice", "Umbraco.ModelsBuilder.Embedded", "Umbraco.Persistence.SqlCe")) + ("Umbraco.Core", "Umbraco.Infrastructure", "Umbraco.Web.UI.NetCore", "Umbraco.Examine.Lucene", "Umbraco.PublishedCache.NuCache", "Umbraco.Web.Common", "Umbraco.Web.Website", "Umbraco.Web.BackOffice", "Umbraco.Persistence.SqlCe")) if ($this.OnError()) { return } }) diff --git a/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs b/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs index 3e0ea9c971..98c0d8674f 100644 --- a/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs +++ b/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Reflection; @@ -19,7 +19,6 @@ namespace Umbraco.Core.Composing "Umbraco.Core", "Umbraco.Infrastructure", "Umbraco.PublishedCache.NuCache", - "Umbraco.ModelsBuilder.Embedded", "Umbraco.Examine.Lucene", "Umbraco.Web.Common", "Umbraco.Web.BackOffice", diff --git a/src/Umbraco.Core/Properties/AssemblyInfo.cs b/src/Umbraco.Core/Properties/AssemblyInfo.cs index ede9e49a7d..9f49dade80 100644 --- a/src/Umbraco.Core/Properties/AssemblyInfo.cs +++ b/src/Umbraco.Core/Properties/AssemblyInfo.cs @@ -8,7 +8,6 @@ using System.Runtime.InteropServices; // Umbraco Cms [assembly: InternalsVisibleTo("Umbraco.Web")] [assembly: InternalsVisibleTo("Umbraco.Web.UI")] -[assembly: InternalsVisibleTo("Umbraco.ModelsBuilder.Embedded")] [assembly: InternalsVisibleTo("Umbraco.Tests")] [assembly: InternalsVisibleTo("Umbraco.Tests.Benchmarks")] diff --git a/src/Umbraco.ModelsBuilder.Embedded/Umbraco.ModelsBuilder.Embedded.csproj b/src/Umbraco.ModelsBuilder.Embedded/Umbraco.ModelsBuilder.Embedded.csproj deleted file mode 100644 index 3d24bd879a..0000000000 --- a/src/Umbraco.ModelsBuilder.Embedded/Umbraco.ModelsBuilder.Embedded.csproj +++ /dev/null @@ -1,37 +0,0 @@ - - - - net5.0 - Library - - - - bin\Release\Umbraco.ModelsBuilder.Embedded.xml - - - - - - - - - - - - - - - - <_Parameter1>Umbraco.Tests - - - <_Parameter1>Umbraco.Tests.UnitTests - - - <_Parameter1>Umbraco.Tests.Benchmarks - - - <_Parameter1>Umbraco.Tests.Integration - - - diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.ModelsBuilder.Embedded/BuilderTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.ModelsBuilder.Embedded/BuilderTests.cs index af77d0e570..ad56863d7f 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.ModelsBuilder.Embedded/BuilderTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.ModelsBuilder.Embedded/BuilderTests.cs @@ -9,8 +9,8 @@ using NUnit.Framework; using Semver; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Models.PublishedContent; -using Umbraco.ModelsBuilder.Embedded; -using Umbraco.ModelsBuilder.Embedded.Building; +using Umbraco.Infrastructure.ModelsBuilder; +using Umbraco.Infrastructure.ModelsBuilder.Building; namespace Umbraco.Tests.UnitTests.Umbraco.ModelsBuilder.Embedded { @@ -61,7 +61,6 @@ using System; using System.Linq.Expressions; using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.PublishedCache; -using Umbraco.ModelsBuilder.Embedded; using Umbraco.Core; namespace Umbraco.Web.PublishedModels @@ -166,7 +165,6 @@ using System; using System.Linq.Expressions; using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.PublishedCache; -using Umbraco.ModelsBuilder.Embedded; using Umbraco.Core; namespace Umbraco.Web.PublishedModels diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.ModelsBuilder.Embedded/UmbracoApplicationTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.ModelsBuilder.Embedded/UmbracoApplicationTests.cs index 0c75318c87..beef5079c4 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.ModelsBuilder.Embedded/UmbracoApplicationTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.ModelsBuilder.Embedded/UmbracoApplicationTests.cs @@ -4,8 +4,8 @@ using System; using System.Collections.Generic; using NUnit.Framework; -using Umbraco.ModelsBuilder.Embedded; -using Umbraco.ModelsBuilder.Embedded.Building; +using Umbraco.Infrastructure.ModelsBuilder; +using Umbraco.Infrastructure.ModelsBuilder.Building; namespace Umbraco.Tests.UnitTests.Umbraco.ModelsBuilder.Embedded { diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj index 25b0c97a1b..a6602c7be3 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj @@ -7,7 +7,6 @@ - diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index c3d3d18451..0c9971816b 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -6,7 +6,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Umbraco.Core.DependencyInjection; using Umbraco.Extensions; -using Umbraco.ModelsBuilder.Embedded.DependencyInjection; using Umbraco.Web.BackOffice.DependencyInjection; using Umbraco.Web.BackOffice.Security; using Umbraco.Web.Common.DependencyInjection; diff --git a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj index b238b3598e..31a9528eca 100644 --- a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj +++ b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj @@ -11,7 +11,6 @@ true - diff --git a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs index b1d21e87b9..320a99e614 100644 --- a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.Options; using Umbraco.Core.DependencyInjection; using Umbraco.Extensions; using Umbraco.Infrastructure.DependencyInjection; -using Umbraco.ModelsBuilder.Embedded.DependencyInjection; +using Umbraco.Web.Common.ModelsBuilder.DependencyInjection; using Umbraco.Web.Common.Routing; using Umbraco.Web.Website.Collections; using Umbraco.Web.Website.Controllers; diff --git a/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj b/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj index 4e898f349e..85de8d6683 100644 --- a/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj +++ b/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj @@ -20,7 +20,6 @@ - diff --git a/src/Umbraco.Web/Properties/AssemblyInfo.cs b/src/Umbraco.Web/Properties/AssemblyInfo.cs index e348aceaee..0ef8fcf488 100644 --- a/src/Umbraco.Web/Properties/AssemblyInfo.cs +++ b/src/Umbraco.Web/Properties/AssemblyInfo.cs @@ -12,7 +12,6 @@ using System.Runtime.InteropServices; // Umbraco Cms [assembly: InternalsVisibleTo("Umbraco.Web.UI")] -[assembly: InternalsVisibleTo("Umbraco.ModelsBuilder.Embedded")] [assembly: InternalsVisibleTo("Umbraco.Tests")] [assembly: InternalsVisibleTo("Umbraco.Tests.Benchmarks")] diff --git a/src/umbraco.sln b/src/umbraco.sln index b7f54cead9..840c7213e6 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -8,15 +8,15 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{2849E9D4-3B4E-40A3-A309-F3CB4F0E125F}" ProjectSection(SolutionItems) = preProject ..\linting\.editorconfig = ..\linting\.editorconfig + ..\build\azure-pipelines.yml = ..\build\azure-pipelines.yml ..\build\build-bootstrap.ps1 = ..\build\build-bootstrap.ps1 ..\build\build.ps1 = ..\build\build.ps1 - ..\NuGet.Config = ..\NuGet.Config - ..\linting\stylecop.json = ..\linting\stylecop.json ..\linting\codeanalysis.ruleset = ..\linting\codeanalysis.ruleset ..\linting\codeanalysis.tests.ruleset = ..\linting\codeanalysis.tests.ruleset ..\Directory.Build.props = ..\Directory.Build.props ..\Directory.Build.targets = ..\Directory.Build.targets - ..\build\azure-pipelines.yml = ..\build\azure-pipelines.yml + ..\NuGet.Config = ..\NuGet.Config + ..\linting\stylecop.json = ..\linting\stylecop.json EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{FD962632-184C-4005-A5F3-E705D92FC645}" @@ -39,8 +39,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NuSpecs", "NuSpecs", "{227C ProjectSection(SolutionItems) = preProject ..\build\NuSpecs\UmbracoCms.Core.nuspec = ..\build\NuSpecs\UmbracoCms.Core.nuspec ..\build\NuSpecs\UmbracoCms.nuspec = ..\build\NuSpecs\UmbracoCms.nuspec - ..\build\NuSpecs\UmbracoCms.Web.nuspec = ..\build\NuSpecs\UmbracoCms.Web.nuspec ..\build\NuSpecs\UmbracoCms.SqlCe.nuspec = ..\build\NuSpecs\UmbracoCms.SqlCe.nuspec + ..\build\NuSpecs\UmbracoCms.Web.nuspec = ..\build\NuSpecs\UmbracoCms.Web.nuspec EndProjectSection EndProject Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Web.UI.Client", "http://localhost:3961", "{3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}" @@ -90,11 +90,11 @@ Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Tests.AcceptanceTes Release.AspNetCompiler.ForceOverwrite = "true" Release.AspNetCompiler.FixedNames = "false" Release.AspNetCompiler.Debug = "False" + SlnRelativePath = "Umbraco.Tests.AcceptanceTest\" DefaultWebSiteLanguage = "Visual C#" StartServerOnDebug = "false" VWDPort = "58896" VWDPort = "62926" - SlnRelativePath = "Umbraco.Tests.AcceptanceTest\" EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Web", "Umbraco.Web\Umbraco.Web.csproj", "{651E1350-91B6-44B7-BD60-7207006D7003}" @@ -113,10 +113,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{E3F9F378 EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{5B03EF4E-E0AC-4905-861B-8C3EC1A0D458}" -ProjectSection(SolutionItems) = preProject - ..\build\NuSpecs\build\Umbraco.Cms.props = ..\build\NuSpecs\build\Umbraco.Cms.props - ..\build\NuSpecs\build\Umbraco.Cms.targets = ..\build\NuSpecs\build\Umbraco.Cms.targets -EndProjectSection + ProjectSection(SolutionItems) = preProject + ..\build\NuSpecs\build\Umbraco.Cms.props = ..\build\NuSpecs\build\Umbraco.Cms.props + ..\build\NuSpecs\build\Umbraco.Cms.targets = ..\build\NuSpecs\build\Umbraco.Cms.targets + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DocTools", "DocTools", "{53594E5B-64A2-4545-8367-E3627D266AE8}" ProjectSection(SolutionItems) = preProject @@ -139,8 +139,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IssueTemplates", "IssueTemp EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Core", "Umbraco.Core\Umbraco.Core.csproj", "{29AA69D9-B597-4395-8D42-43B1263C240A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.ModelsBuilder.Embedded", "Umbraco.ModelsBuilder.Embedded\Umbraco.ModelsBuilder.Embedded.csproj", "{52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Infrastructure", "Umbraco.Infrastructure\Umbraco.Infrastructure.csproj", "{3AE7BF57-966B-45A5-910A-954D7C554441}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Persistence.SqlCe", "Umbraco.Persistence.SqlCe\Umbraco.Persistence.SqlCe.csproj", "{33085570-9BF2-4065-A9B0-A29D920D13BA}" @@ -195,10 +193,6 @@ Global {29AA69D9-B597-4395-8D42-43B1263C240A}.Debug|Any CPU.Build.0 = Debug|Any CPU {29AA69D9-B597-4395-8D42-43B1263C240A}.Release|Any CPU.ActiveCfg = Release|Any CPU {29AA69D9-B597-4395-8D42-43B1263C240A}.Release|Any CPU.Build.0 = Release|Any CPU - {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Release|Any CPU.Build.0 = Release|Any CPU {3AE7BF57-966B-45A5-910A-954D7C554441}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3AE7BF57-966B-45A5-910A-954D7C554441}.Debug|Any CPU.Build.0 = Debug|Any CPU {3AE7BF57-966B-45A5-910A-954D7C554441}.Release|Any CPU.ActiveCfg = Release|Any CPU From 0fe7ad826dbc62263622361eac481004ea3d08db Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Fri, 19 Feb 2021 17:03:48 +0000 Subject: [PATCH 67/72] Updated membership successful result as per PR comments --- .../Controllers/MemberController.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index fa8788f14d..cf5681d578 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -268,10 +268,20 @@ namespace Umbraco.Web.BackOffice.Controllers switch (contentItem.Action) { case ContentSaveAction.Save: - Task> updateSuccessful = UpdateMemberAsync(contentItem); + ActionResult updateSuccessful = await UpdateMemberAsync(contentItem); + if (!(updateSuccessful.Result is null)) + { + return updateSuccessful.Result; + } + break; case ContentSaveAction.SaveNew: - Task> createSuccessful = CreateMemberAsync(contentItem); + ActionResult createSuccessful = await CreateMemberAsync(contentItem); + if (!(createSuccessful.Result is null)) + { + return createSuccessful.Result; + } + break; default: // we don't support anything else for members From c453719c2d9b069eb6e6b1d8fab050bd31f7f496 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Feb 2021 09:49:24 +0100 Subject: [PATCH 68/72] Fix the register of the IModelsBuilderDashboardProvider --- .../UmbracoBuilderExtensions.cs | 4 +++- .../ModelsBuilderDashboardProvider.cs | 3 +-- .../ModelsBuilder/UmbracoBuilderExtensions.cs | 14 ++++++++++---- .../UmbracoBuilderDependencyInjectionExtensions.cs | 7 ++++++- .../NoopModelsBuilderDashboardProvider.cs | 6 ------ 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index 4baf6a29f5..30cc0ae67b 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.BackOffice.Controllers; using Umbraco.Cms.Web.BackOffice.Filters; using Umbraco.Cms.Web.BackOffice.Middleware; +using Umbraco.Cms.Web.BackOffice.ModelsBuilder; using Umbraco.Cms.Web.BackOffice.Routing; using Umbraco.Cms.Web.BackOffice.Security; using Umbraco.Cms.Web.BackOffice.Services; @@ -43,7 +44,8 @@ namespace Umbraco.Extensions .AddWebServer() .AddPreviewSupport() .AddHostedServices() - .AddDistributedCache(); + .AddDistributedCache() + .AddModelsBuilderDashboard(); /// /// Adds Umbraco back office authentication requirements diff --git a/src/Umbraco.Web.BackOffice/ModelsBuilder/ModelsBuilderDashboardProvider.cs b/src/Umbraco.Web.BackOffice/ModelsBuilder/ModelsBuilderDashboardProvider.cs index 377cf0bc4c..6d1a7a13fc 100644 --- a/src/Umbraco.Web.BackOffice/ModelsBuilder/ModelsBuilderDashboardProvider.cs +++ b/src/Umbraco.Web.BackOffice/ModelsBuilder/ModelsBuilderDashboardProvider.cs @@ -1,9 +1,8 @@ using Microsoft.AspNetCore.Routing; -using Umbraco.Cms.Web.BackOffice.ModelsBuilder; using Umbraco.Extensions; using Umbraco.Web.Common.ModelsBuilder; -namespace Umbraco.Web.BackOffice.ModelsBuilder +namespace Umbraco.Cms.Web.BackOffice.ModelsBuilder { public class ModelsBuilderDashboardProvider: IModelsBuilderDashboardProvider { diff --git a/src/Umbraco.Web.BackOffice/ModelsBuilder/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/ModelsBuilder/UmbracoBuilderExtensions.cs index 749dba2397..4e372d8e62 100644 --- a/src/Umbraco.Web.BackOffice/ModelsBuilder/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/ModelsBuilder/UmbracoBuilderExtensions.cs @@ -1,25 +1,31 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Web.BackOffice.ModelsBuilder; using Umbraco.Extensions; using Umbraco.Web.Common.ModelsBuilder; -namespace Umbraco.Web.BackOffice.ModelsBuilder +namespace Umbraco.Cms.Web.BackOffice.ModelsBuilder { /// /// Extension methods for for the common Umbraco functionality /// public static class UmbracoBuilderExtensions { + /// + /// Adds the ModelsBuilder dashboard. + /// + public static IUmbracoBuilder AddModelsBuilderDashboard(this IUmbracoBuilder builder) + { + builder.Services.AddUnique(); + return builder; + } + /// /// Can be called if using an external models builder to remove the embedded models builder controller features /// public static IUmbracoBuilder DisableModelsBuilderControllers(this IUmbracoBuilder builder) { builder.Services.AddSingleton(); - builder.Services.AddUnique(); return builder; } - } } diff --git a/src/Umbraco.Web.Common/ModelsBuilder/DependencyInjection/UmbracoBuilderDependencyInjectionExtensions.cs b/src/Umbraco.Web.Common/ModelsBuilder/DependencyInjection/UmbracoBuilderDependencyInjectionExtensions.cs index 44e7d8debe..7cbbf3c9c7 100644 --- a/src/Umbraco.Web.Common/ModelsBuilder/DependencyInjection/UmbracoBuilderDependencyInjectionExtensions.cs +++ b/src/Umbraco.Web.Common/ModelsBuilder/DependencyInjection/UmbracoBuilderDependencyInjectionExtensions.cs @@ -1,3 +1,4 @@ +using System.Linq; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -125,7 +126,11 @@ namespace Umbraco.Extensions } }); - builder.Services.AddUnique(); + + if (!builder.Services.Any(x=>x.ServiceType == typeof(IModelsBuilderDashboardProvider))) + { + builder.Services.AddUnique(); + } return builder; } diff --git a/src/Umbraco.Web.Common/ModelsBuilder/NoopModelsBuilderDashboardProvider.cs b/src/Umbraco.Web.Common/ModelsBuilder/NoopModelsBuilderDashboardProvider.cs index 7c5a0daabf..bc9e9d32a7 100644 --- a/src/Umbraco.Web.Common/ModelsBuilder/NoopModelsBuilderDashboardProvider.cs +++ b/src/Umbraco.Web.Common/ModelsBuilder/NoopModelsBuilderDashboardProvider.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Umbraco.Web.Common.ModelsBuilder { public class NoopModelsBuilderDashboardProvider: IModelsBuilderDashboardProvider From 890cd45677f2ae14dbe6eced093eaf38834d4ab5 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Feb 2021 10:09:20 +0100 Subject: [PATCH 69/72] Fix namespace --- src/Umbraco.Core/IO/ViewHelper.cs | 2 +- .../Services/Implement/FileService.cs | 2 +- src/Umbraco.TestData/LoadTestController.cs | 12 +- .../cypress/integration/Content/content.ts | 5 +- .../integration/Settings/partialsViews.ts | 6 +- .../cypress/integration/Settings/templates.ts | 6 +- .../Repositories/TemplateRepositoryTest.cs | 6 +- .../Importing/StandardMvc-Package.xml | 166 +++++++++--------- .../Umbraco.Core/Templates/ViewHelperTests.cs | 14 +- .../Views/UmbracoViewPageTests.cs | 2 +- .../Macros/PartialViewMacroPage.cs | 2 +- .../Views/UmbracoViewPage.cs | 2 +- .../Views/Partials/blocklist/default.cshtml | 2 +- .../Partials/grid/bootstrap3-fluid.cshtml | 2 +- .../Views/Partials/grid/bootstrap3.cshtml | 2 +- .../Views/Partials/grid/editors/embed.cshtml | 3 +- .../Views/Partials/grid/editors/macro.cshtml | 2 +- 17 files changed, 117 insertions(+), 119 deletions(-) diff --git a/src/Umbraco.Core/IO/ViewHelper.cs b/src/Umbraco.Core/IO/ViewHelper.cs index 9a7016b6be..258c4a7f64 100644 --- a/src/Umbraco.Core/IO/ViewHelper.cs +++ b/src/Umbraco.Core/IO/ViewHelper.cs @@ -71,7 +71,7 @@ namespace Umbraco.Cms.Core.IO // @inherits Umbraco.Web.Mvc.UmbracoViewPage // @inherits Umbraco.Web.Mvc.UmbracoViewPage content.AppendLine("@using Umbraco.Cms.Web.Common.PublishedModels;"); - content.Append("@inherits Umbraco.Cms.Web.Common.AspNetCore.UmbracoViewPage"); + content.Append("@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage"); if (modelClassName.IsNullOrWhiteSpace() == false) { content.Append("<"); diff --git a/src/Umbraco.Infrastructure/Services/Implement/FileService.cs b/src/Umbraco.Infrastructure/Services/Implement/FileService.cs index acab6caae4..ff42a35a25 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/FileService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/FileService.cs @@ -33,7 +33,7 @@ namespace Umbraco.Core.Services.Implement private readonly GlobalSettings _globalSettings; private readonly IHostingEnvironment _hostingEnvironment; - private const string PartialViewHeader = "@inherits Umbraco.Web.Common.Views.UmbracoViewPage"; + private const string PartialViewHeader = "@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage"; private const string PartialViewMacroHeader = "@inherits Umbraco.Web.Common.Macros.PartialViewMacroPage"; public FileService(IScopeProvider uowProvider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, diff --git a/src/Umbraco.TestData/LoadTestController.cs b/src/Umbraco.TestData/LoadTestController.cs index 6cbe31d70e..817b7a1d76 100644 --- a/src/Umbraco.TestData/LoadTestController.cs +++ b/src/Umbraco.TestData/LoadTestController.cs @@ -1,14 +1,12 @@ using System; -using System.Threading; +using System.Configuration; +using System.Diagnostics; using System.Linq; -using System.Web.Mvc; -using Umbraco.Core.Services; -using Umbraco.Core.Models; +using System.Threading; using System.Web; using System.Web.Hosting; +using System.Web.Mvc; using System.Web.Routing; -using System.Diagnostics; -using System.Configuration; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.DependencyInjection; @@ -63,7 +61,7 @@ namespace Umbraco.TestData "; private static readonly string _containerTemplateText = @" -@inherits Umbraco.Web.Mvc.UmbracoViewPage +@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage @{ Layout = null; var container = Umbraco.ContentAtRoot().OfTypes(""" + _containerAlias + @""").FirstOrDefault(); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts index 22f1f883d0..c862708bbe 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts @@ -1,5 +1,6 @@ /// -import { DocumentTypeBuilder, ContentBuilder, AliasHelper } from 'umbraco-cypress-testhelpers'; +import {AliasHelper, ContentBuilder, DocumentTypeBuilder} from 'umbraco-cypress-testhelpers'; + context('Content', () => { beforeEach(() => { @@ -558,7 +559,7 @@ context('Content', () => { cy.saveDocumentType(pickerDocType); // Edit it the template to allow us to verify the rendered view. - cy.editTemplate(pickerDocTypeName, `@inherits Umbraco.Web.Mvc.UmbracoViewPage + cy.editTemplate(pickerDocTypeName, `@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage @using ContentModels = Umbraco.Web.PublishedModels; @{ Layout = null; diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/partialsViews.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/partialsViews.ts index 068338f8fa..f664123d3b 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/partialsViews.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/partialsViews.ts @@ -1,5 +1,5 @@ /// -import { PartialViewBuilder } from "umbraco-cypress-testhelpers"; +import {PartialViewBuilder} from "umbraco-cypress-testhelpers"; context('Partial Views', () => { @@ -95,7 +95,7 @@ context('Partial Views', () => { // Build and save partial view const partialView = new PartialViewBuilder() .withName(name) - .withContent("@inherits Umbraco.Web.Mvc.UmbracoViewPage") + .withContent("@inherits UUmbraco.Cms.Web.Common.Views.UmbracoViewPage") .build(); cy.savePartialView(partialView); @@ -123,7 +123,7 @@ context('Partial Views', () => { const partialView = new PartialViewBuilder() .withName(name) - .withContent("@inherits Umbraco.Web.Mvc.UmbracoViewPage\n") + .withContent("@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage\n") .build(); cy.savePartialView(partialView); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts index c586384af7..3122c3ebf7 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts @@ -1,5 +1,5 @@ /// -import { TemplateBuilder } from 'umbraco-cypress-testhelpers'; +import {TemplateBuilder} from 'umbraco-cypress-testhelpers'; context('Templates', () => { @@ -54,7 +54,7 @@ context('Templates', () => { const template = new TemplateBuilder() .withName(name) - .withContent('@inherits Umbraco.Web.Mvc.UmbracoViewPage\n') + .withContent('@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage\n') .build(); cy.saveTemplate(template); @@ -87,7 +87,7 @@ context('Templates', () => { const template = new TemplateBuilder() .withName(name) - .withContent('@inherits Umbraco.Web.Mvc.UmbracoViewPage\n') + .withContent('@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage\n') .build(); cy.saveTemplate(template); diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs index e704b82bdd..b323abf472 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Text; using Microsoft.Extensions.Logging; using NUnit.Framework; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; @@ -24,7 +25,6 @@ using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Scoping; using Umbraco.Core.Serialization; using Umbraco.Extensions; -using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repositories { @@ -95,7 +95,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos Assert.That(repository.Get("test"), Is.Not.Null); Assert.That(FileSystems.MvcViewsFileSystem.FileExists("test.cshtml"), Is.True); Assert.AreEqual( - @"@usingUmbraco.Cms.Web.Common.PublishedModels;@inheritsUmbraco.Cms.Web.Common.AspNetCore.UmbracoViewPage@{Layout=null;}".StripWhitespace(), + @"@usingUmbraco.Cms.Web.Common.PublishedModels;@inheritsUmbraco.Cms.Web.Common.Views.UmbracoViewPage@{Layout=null;}".StripWhitespace(), template.Content.StripWhitespace()); } } @@ -123,7 +123,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos Assert.That(repository.Get("test2"), Is.Not.Null); Assert.That(FileSystems.MvcViewsFileSystem.FileExists("test2.cshtml"), Is.True); Assert.AreEqual( - "@usingUmbraco.Cms.Web.Common.PublishedModels;@inherits Umbraco.Cms.Web.Common.AspNetCore.UmbracoViewPage @{ Layout = \"test.cshtml\";}".StripWhitespace(), + "@usingUmbraco.Cms.Web.Common.PublishedModels;@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage @{ Layout = \"test.cshtml\";}".StripWhitespace(), template2.Content.StripWhitespace()); } } diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/Importing/StandardMvc-Package.xml b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/Importing/StandardMvc-Package.xml index ee6f7cea4a..a4787dd570 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/Importing/StandardMvc-Package.xml +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/Importing/StandardMvc-Package.xml @@ -189,17 +189,17 @@ http://www.creativefounds.co.uk - @@ -1099,7 +1099,7 @@ Google Maps - A map macro that you can use within Rich Text Areas Articles SW_Master -
- + @Html.Raw(Model.Content.GetPropertyValue("bodyText")) - +
    @foreach (var item in nodes.Skip((page - 1) * pageSize).Take(pageSize)) { @@ -1172,9 +1172,9 @@ Google Maps - A map macro that you can use within Rich Text Areas

    @item.GetPropertyValue("articleSummary") - +

    - + }
@@ -1185,7 +1185,7 @@ Google Maps - A map macro that you can use within Rich Text Areas @for (int p = 1; p < totalPages + 1; p++) { //string selected = (p == page) ? "selected" : String.Empty; - //
  • @p
  • + //
  • @p
  • @p if (p < totalPages) { @@ -1197,7 +1197,7 @@ Google Maps - A map macro that you can use within Rich Text Areas
    - + @Html.Partial("ContentPanels",@Model.Content) ]]>
    @@ -1207,13 +1207,13 @@ Google Maps - A map macro that you can use within Rich Text Areas ClientAreas SW_Master - x.IsVisible() && x.TemplateId > 0 && Umbraco.MemberHasAccess(x.Id, x.Path)); }
    - + - - + +
    ]]>
    @@ -1243,7 +1243,7 @@ Google Maps - A map macro that you can use within Rich Text Areas Layout = "SW_Master.cshtml"; }
    - + ]]> @@ -1266,9 +1266,9 @@ Google Maps - A map macro that you can use within Rich Text Areas @{ Layout = "SW_Master.cshtml"; } - +
    -
      +
        @{ var nodeIds = Model.Content.GetPropertyValue("slideshow").ToString().Split(','); List slides = new List(); @@ -1313,7 +1313,7 @@ Google Maps - A map macro that you can use within Rich Text Areas
    - +
    @Html.Raw(Model.Content.GetPropertyValue("panelContent1").ToString()) @@ -1345,7 +1345,7 @@ Google Maps - A map macro that you can use within Rich Text Areas } }
    - + ]]> @@ -1420,13 +1420,13 @@ Google Maps - A map macro that you can use within Rich Text Areas page = 1; } } - + }
    @@ -1470,14 +1470,14 @@ Google Maps - A map macro that you can use within Rich Text Areas

    @Html.Raw(searchHiglight)

    - + } } @@ -1489,7 +1489,7 @@ Google Maps - A map macro that you can use within Rich Text Areas @for (int p = 1; p < totalPages + 1; p++) { //string selected = (p == page) ? "selected" : String.Empty; - //
  • @p
  • + //
  • @p
  • @p if (p < totalPages) { @@ -1514,7 +1514,7 @@ Google Maps - A map macro that you can use within Rich Text Areas Layout = "SW_Master.cshtml"; }
    - + @helper traverse(IPublishedContent node) { - var cc = node.Children.Where(x=>x.IsVisible() && x.TemplateId > 0); + var cc = node.Children.Where(x=>x.IsVisible() && x.TemplateId > 0); if (cc.Count()>0) {
      @@ -1560,7 +1560,7 @@ Google Maps - A map macro that you can use within Rich Text Areas Layout = "SW_Master.cshtml"; }
      - + ]]> @@ -1590,27 +1590,27 @@ Google Maps - A map macro that you can use within Rich Text Areas @Model.Content.GetPropertyValue("title") - - + + - + - +