diff --git a/.gitignore b/.gitignore index 95295e44c4..1c54100176 100644 --- a/.gitignore +++ b/.gitignore @@ -201,4 +201,6 @@ src/Umbraco.Tests/TEMP/ /src/Umbraco.Web.UI/config/umbracoSettings.config /src/Umbraco.Web.UI.NetCore/Umbraco/models/* +src/Umbraco.Tests.UnitTests/umbraco/Data/TEMP/ /src/Umbraco.Web.UI.NetCore/appsettings.Local.json +src/Umbraco.Tests.Integration/DatabaseContextTests.sdf diff --git a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs index 92aab36bd4..0d3a5b7536 100644 --- a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs @@ -119,7 +119,7 @@ namespace Umbraco.Cms.Core.Models.Mapping Value = _localizedTextService.UmbracoDictionaryTranslate(CultureDictionary, member.ContentType.Name), View = _propertyEditorCollection[Constants.PropertyEditors.Aliases.Label].GetValueEditor().View }, - GetLoginProperty(_memberTypeService, member, _localizedTextService), + GetLoginProperty(member, _localizedTextService), new ContentPropertyDisplay { Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email", @@ -208,7 +208,6 @@ namespace Umbraco.Cms.Core.Models.Mapping /// /// Returns the login property display field /// - /// /// /// /// @@ -218,7 +217,7 @@ namespace Umbraco.Cms.Core.Models.Mapping /// the membership provider is a custom one, we cannot allow changing the username because MembershipProvider's do not actually natively /// allow that. /// - internal static ContentPropertyDisplay GetLoginProperty(IMemberTypeService memberTypeService, IMember member, ILocalizedTextService localizedText) + internal static ContentPropertyDisplay GetLoginProperty(IMember member, ILocalizedTextService localizedText) { var prop = new ContentPropertyDisplay { @@ -234,7 +233,7 @@ namespace Umbraco.Cms.Core.Models.Mapping internal IDictionary GetMemberGroupValue(string username) { - var userRoles = username.IsNullOrWhiteSpace() ? null : _memberService.GetAllRoles(username); + IEnumerable userRoles = username.IsNullOrWhiteSpace() ? null : _memberService.GetAllRoles(username); // create a dictionary of all roles (except internal roles) + "false" var result = _memberGroupService.GetAll() @@ -245,11 +244,16 @@ namespace Umbraco.Cms.Core.Models.Mapping .ToDictionary(x => x, x => false); // if user has no roles, just return the dictionary - if (userRoles == null) return result; + if (userRoles == null) + { + return result; + } // else update the dictionary to "true" for the user roles (except internal roles) foreach (var userRole in userRoles.Where(x => x.StartsWith(Constants.Conventions.Member.InternalRolePrefix) == false)) + { result[userRole] = true; + } return result; } diff --git a/src/Umbraco.Core/Services/IMemberGroupService.cs b/src/Umbraco.Core/Services/IMemberGroupService.cs index e584537ab1..16028ded3f 100644 --- a/src/Umbraco.Core/Services/IMemberGroupService.cs +++ b/src/Umbraco.Core/Services/IMemberGroupService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Umbraco.Cms.Core.Models; diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs index 406eb08c62..e97add3f5e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs @@ -314,7 +314,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // persist the member dto dto.NodeId = nodeDto.NodeId; - // TODO: password parts of this file need updating // if the password is empty, generate one with the special prefix // this will hash the guid with a salt so should be nicely random if (entity.RawPasswordValue.IsNullOrWhiteSpace()) diff --git a/src/Umbraco.Infrastructure/Security/IMemberManager.cs b/src/Umbraco.Infrastructure/Security/IMemberManager.cs index 85b4c0c300..b310e9434f 100644 --- a/src/Umbraco.Infrastructure/Security/IMemberManager.cs +++ b/src/Umbraco.Infrastructure/Security/IMemberManager.cs @@ -3,7 +3,7 @@ namespace Umbraco.Cms.Core.Security /// /// The user manager for members /// - public interface IMemberManager : IUmbracoUserManager + public interface IMemberManager : IUmbracoUserManager { } } diff --git a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs index 30d435f345..0cf724ec20 100644 --- a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs +++ b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs @@ -48,10 +48,10 @@ namespace Umbraco.Cms.Core.Security target.EnableChangeTracking(); }); - mapper.Define( + mapper.Define( (source, context) => { - var target = new MembersIdentityUser(source.Id); + var target = new MemberIdentityUser(source.Id); target.DisableChangeTracking(); return target; }, @@ -100,7 +100,7 @@ namespace Umbraco.Cms.Core.Security //target.Roles =; } - private void Map(IMember source, MembersIdentityUser target) + private void Map(IMember source, MemberIdentityUser target) { target.Email = source.Email; target.UserName = source.Username; diff --git a/src/Umbraco.Infrastructure/Security/MembersIdentityBuilder.cs b/src/Umbraco.Infrastructure/Security/MemberIdentityBuilder.cs similarity index 71% rename from src/Umbraco.Infrastructure/Security/MembersIdentityBuilder.cs rename to src/Umbraco.Infrastructure/Security/MemberIdentityBuilder.cs index 726b999b89..4e2e4a39b1 100644 --- a/src/Umbraco.Infrastructure/Security/MembersIdentityBuilder.cs +++ b/src/Umbraco.Infrastructure/Security/MemberIdentityBuilder.cs @@ -5,18 +5,18 @@ using Microsoft.Extensions.DependencyInjection; namespace Umbraco.Cms.Core.Security { - public class MembersIdentityBuilder : IdentityBuilder + public class MemberIdentityBuilder : IdentityBuilder { - public MembersIdentityBuilder(IServiceCollection services) : base(typeof(MembersIdentityUser), services) + public MemberIdentityBuilder(IServiceCollection services) : base(typeof(MemberIdentityUser), services) { } - public MembersIdentityBuilder(Type role, IServiceCollection services) : base(typeof(MembersIdentityUser), role, services) + public MemberIdentityBuilder(Type role, IServiceCollection services) : base(typeof(MemberIdentityUser), 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. @@ -27,7 +27,7 @@ namespace Umbraco.Cms.Core.Security { 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/MemberIdentityOptions.cs similarity index 78% rename from src/Umbraco.Infrastructure/Security/MembersIdentityOptions.cs rename to src/Umbraco.Infrastructure/Security/MemberIdentityOptions.cs index 8f993a1f76..4e05797a04 100644 --- a/src/Umbraco.Infrastructure/Security/MembersIdentityOptions.cs +++ b/src/Umbraco.Infrastructure/Security/MemberIdentityOptions.cs @@ -5,7 +5,7 @@ namespace Umbraco.Cms.Core.Security /// /// Identity options specifically for the Umbraco members identity implementation /// - public class MembersIdentityOptions : IdentityOptions + public class MemberIdentityOptions : IdentityOptions { } } diff --git a/src/Umbraco.Infrastructure/Security/MembersIdentityUser.cs b/src/Umbraco.Infrastructure/Security/MemberIdentityUser.cs similarity index 90% rename from src/Umbraco.Infrastructure/Security/MembersIdentityUser.cs rename to src/Umbraco.Infrastructure/Security/MemberIdentityUser.cs index 6e3473c3ce..539234ac65 100644 --- a/src/Umbraco.Infrastructure/Security/MembersIdentityUser.cs +++ b/src/Umbraco.Infrastructure/Security/MemberIdentityUser.cs @@ -11,7 +11,7 @@ namespace Umbraco.Cms.Core.Security /// /// The identity user used for the member /// - public class MembersIdentityUser : UmbracoIdentityUser + public class MemberIdentityUser : UmbracoIdentityUser { private string _name; private string _passwordConfig; @@ -23,29 +23,29 @@ namespace Umbraco.Cms.Core.Security groups => groups.GetHashCode()); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public MembersIdentityUser(int userId) + public MemberIdentityUser(int userId) { // use the property setters - they do more than just setting a field Id = UserIdToString(userId); } - public MembersIdentityUser() + public MemberIdentityUser() { } /// /// Used to construct a new instance without an identity /// - public static MembersIdentityUser CreateNew(string username, string email, string memberTypeAlias, string name = null) + public static MemberIdentityUser 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(); + var user = new MemberIdentityUser(); user.DisableChangeTracking(); user.UserName = username; user.Email = email; diff --git a/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs b/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs new file mode 100644 index 0000000000..279735bfa2 --- /dev/null +++ b/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs @@ -0,0 +1,272 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Identity; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Core.Security +{ + /// + /// A custom user store that uses Umbraco member data + /// + public class MemberRoleStore : IRoleStore + { + private readonly IMemberGroupService _memberGroupService; + private bool _disposed; + + //TODO: Move into custom error describer. + //TODO: How revealing can the error messages be? + private readonly IdentityError _intParseError = new IdentityError { Code = "IdentityIdParseError", Description = "Cannot parse ID to int" }; + private readonly IdentityError _memberGroupNotFoundError = new IdentityError { Code = "IdentityMemberGroupNotFound", Description = "Member group not found" }; + private const string genericIdentityErrorCode = "IdentityErrorUserStore"; + + public MemberRoleStore(IMemberGroupService memberGroupService, IdentityErrorDescriber errorDescriber) + { + _memberGroupService = memberGroupService ?? throw new ArgumentNullException(nameof(memberGroupService)); + ErrorDescriber = errorDescriber ?? throw new ArgumentNullException(nameof(errorDescriber)); + } + + /// + /// Gets or sets the for any error that occurred with the current operation. + /// + public IdentityErrorDescriber ErrorDescriber { get; set; } + + /// + public Task CreateAsync(UmbracoIdentityRole role, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + + var memberGroup = new MemberGroup + { + Name = role.Name + }; + + _memberGroupService.Save(memberGroup); + + role.Id = memberGroup.Id.ToString(); + + return Task.FromResult(IdentityResult.Success); + } + + + /// + public Task UpdateAsync(UmbracoIdentityRole role, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + + if (!int.TryParse(role.Id, out int roleId)) + { + return Task.FromResult(IdentityResult.Failed(_intParseError)); + } + + IMemberGroup memberGroup = _memberGroupService.GetById(roleId); + if (memberGroup != null) + { + if (MapToMemberGroup(role, memberGroup)) + { + _memberGroupService.Save(memberGroup); + } + + return Task.FromResult(IdentityResult.Success); + } + else + { + return Task.FromResult(IdentityResult.Failed(_memberGroupNotFoundError)); + } + } + + /// + public Task DeleteAsync(UmbracoIdentityRole role, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + + if (!int.TryParse(role.Id, out int roleId)) + { + throw new ArgumentException("The Id of the role is not an integer"); + } + + IMemberGroup memberGroup = _memberGroupService.GetById(roleId); + if (memberGroup != null) + { + _memberGroupService.Delete(memberGroup); + } + else + { + return Task.FromResult(IdentityResult.Failed(_memberGroupNotFoundError)); + } + + return Task.FromResult(IdentityResult.Success); + } + + /// + public Task GetRoleIdAsync(UmbracoIdentityRole role, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + + return Task.FromResult(role.Id); + } + + /// + public Task GetRoleNameAsync(UmbracoIdentityRole role, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + + return Task.FromResult(role.Name); + } + + /// + public Task SetRoleNameAsync(UmbracoIdentityRole role, string roleName, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + role.Name = roleName; + return Task.CompletedTask; + } + + /// + public Task GetNormalizedRoleNameAsync(UmbracoIdentityRole role, CancellationToken cancellationToken = default) + => GetRoleNameAsync(role, cancellationToken); + + /// + public Task SetNormalizedRoleNameAsync(UmbracoIdentityRole role, string normalizedName, CancellationToken cancellationToken = default) + => SetRoleNameAsync(role, normalizedName, cancellationToken); + + /// + public Task FindByIdAsync(string roleId, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (string.IsNullOrWhiteSpace(roleId)) + { + throw new ArgumentNullException(nameof(roleId)); + } + + IMemberGroup memberGroup; + + // member group can be found by int or Guid, so try both + if (!int.TryParse(roleId, out int id)) + { + if (!Guid.TryParse(roleId, out Guid guid)) + { + throw new ArgumentOutOfRangeException(nameof(roleId), $"{nameof(roleId)} is not a valid Guid"); + } + else + { + memberGroup = _memberGroupService.GetById(guid); + } + } + else + { + memberGroup = _memberGroupService.GetById(id); + } + + return Task.FromResult(memberGroup == null ? null : MapFromMemberGroup(memberGroup)); + } + + /// + public Task FindByNameAsync(string name, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + IMemberGroup memberGroup = _memberGroupService.GetByName(name); + return Task.FromResult(memberGroup == null ? null : MapFromMemberGroup(memberGroup)); + } + + /// + /// Maps a member group to an identity role + /// + /// + /// + private UmbracoIdentityRole MapFromMemberGroup(IMemberGroup memberGroup) + { + var result = new UmbracoIdentityRole + { + Id = memberGroup.Id.ToString(), + Name = memberGroup.Name + // TODO: Implement this functionality, requires DB and logic updates + //ConcurrencyStamp + }; + return result; + } + + /// + /// Map an identity role to a member group + /// + /// + /// + /// + private bool MapToMemberGroup(UmbracoIdentityRole role, IMemberGroup memberGroup) + { + var anythingChanged = false; + + if (role.IsPropertyDirty(nameof(UmbracoIdentityRole.Name)) + && !string.IsNullOrEmpty(role.Name) && memberGroup.Name != role.Name) + { + // TODO: Need to support ConcurrencyStamp and logic + + memberGroup.Name = role.Name; + anythingChanged = true; + } + + return anythingChanged; + } + + /// + /// Dispose the store + /// + public void Dispose() => _disposed = true; + + /// + /// Throws if this class has been disposed. + /// + protected void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name); + } + } + } +} diff --git a/src/Umbraco.Infrastructure/Security/MemberRolesUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberRolesUserStore.cs deleted file mode 100644 index 65b3cf1ef9..0000000000 --- a/src/Umbraco.Infrastructure/Security/MemberRolesUserStore.cs +++ /dev/null @@ -1,57 +0,0 @@ -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.Cms.Core.Scoping; -using Umbraco.Cms.Core.Services; - -namespace Umbraco.Cms.Core.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/MemberUserStore.cs similarity index 59% rename from src/Umbraco.Infrastructure/Security/MembersUserStore.cs rename to src/Umbraco.Infrastructure/Security/MemberUserStore.cs index 6b4a735988..c0b9a19ef1 100644 --- a/src/Umbraco.Infrastructure/Security/MembersUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs @@ -19,20 +19,21 @@ namespace Umbraco.Cms.Core.Security /// /// A custom user store that uses Umbraco member data /// - public class MembersUserStore : UserStoreBase, string, IdentityUserClaim, IdentityUserRole, IdentityUserLogin, IdentityUserToken, IdentityRoleClaim> + public class MemberUserStore : UserStoreBase, IdentityUserRole, IdentityUserLogin, IdentityUserToken, IdentityRoleClaim> { + private const string genericIdentityErrorCode = "IdentityErrorUserStore"; private readonly IMemberService _memberService; private readonly UmbracoMapper _mapper; 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 /// The error describer - public MembersUserStore(IMemberService memberService, UmbracoMapper mapper, IScopeProvider scopeProvider, IdentityErrorDescriber describer) + public MemberUserStore(IMemberService memberService, UmbracoMapper mapper, IScopeProvider scopeProvider, IdentityErrorDescriber describer) : base(describer) { _memberService = memberService ?? throw new ArgumentNullException(nameof(memberService)); @@ -40,183 +41,213 @@ namespace Umbraco.Cms.Core.Security _scopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider)); } + //TODO: why is this not supported? /// /// Not supported in Umbraco /// /// [EditorBrowsable(EditorBrowsableState.Never)] - public override IQueryable Users => throw new NotImplementedException(); + public override IQueryable Users => throw new NotImplementedException(); /// - public override Task GetNormalizedUserNameAsync(MembersIdentityUser user, CancellationToken cancellationToken) => GetUserNameAsync(user, cancellationToken); + public override Task GetNormalizedUserNameAsync(MemberIdentityUser user, CancellationToken cancellationToken = default) => GetUserNameAsync(user, cancellationToken); /// - public override Task SetNormalizedUserNameAsync(MembersIdentityUser user, string normalizedName, CancellationToken cancellationToken) => SetUserNameAsync(user, normalizedName, cancellationToken); + public override Task SetNormalizedUserNameAsync(MemberIdentityUser user, string normalizedName, CancellationToken cancellationToken = default) => SetUserNameAsync(user, normalizedName, cancellationToken); /// - public override Task CreateAsync(MembersIdentityUser user, CancellationToken cancellationToken = default) + public override Task CreateAsync(MemberIdentityUser user, CancellationToken cancellationToken = default) { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) + try { - throw new ArgumentNullException(nameof(user)); - } - - // create member - IMember memberEntity = _memberService.CreateMember( - user.UserName, - user.Email, - user.Name.IsNullOrWhiteSpace() ? user.UserName : user.Name, - user.MemberTypeAlias.IsNullOrWhiteSpace() ? Constants.Security.DefaultMemberTypeAlias : user.MemberTypeAlias); - - UpdateMemberProperties(memberEntity, user); - - // create the member - _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. - // var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(MembersIdentityUser.Logins)); - // TODO: confirm re externallogins implementation - //if (isLoginsPropertyDirty) - //{ - // _externalLoginService.Save( - // user.Id, - // user.Logins.Select(x => new ExternalLogin( - // x.LoginProvider, - // x.ProviderKey, - // x.UserData))); - //} - - return Task.FromResult(IdentityResult.Success); - } - - /// - public override Task UpdateAsync(MembersIdentityUser user, CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - Attempt asInt = user.Id.TryConvertTo(); - if (asInt == false) - { - throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); - } - - using (IScope scope = _scopeProvider.CreateScope()) - { - IMember found = _memberService.GetById(asInt.Result); - if (found != null) + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) { - // we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. - var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(MembersIdentityUser.Logins)); - - if (UpdateMemberProperties(found, user)) - { - _memberService.Save(found); - } - - // TODO: when to implement external login service? - - //if (isLoginsPropertyDirty) - //{ - // _externalLoginService.Save( - // found.Id, - // user.Logins.Select(x => new ExternalLogin( - // x.LoginProvider, - // x.ProviderKey, - // x.UserData))); - //} + throw new ArgumentNullException(nameof(user)); } - scope.Complete(); - } + // create member + IMember memberEntity = _memberService.CreateMember( + user.UserName, + user.Email, + user.Name.IsNullOrWhiteSpace() ? user.UserName : user.Name, + user.MemberTypeAlias.IsNullOrWhiteSpace() ? Constants.Security.DefaultMemberTypeAlias : user.MemberTypeAlias); - return Task.FromResult(IdentityResult.Success); + UpdateMemberProperties(memberEntity, user); + + // create the member + _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. + // var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(MembersIdentityUser.Logins)); + // TODO: confirm re externallogins implementation + //if (isLoginsPropertyDirty) + //{ + // _externalLoginService.Save( + // user.Id, + // user.Logins.Select(x => new ExternalLogin( + // x.LoginProvider, + // x.ProviderKey, + // x.UserData))); + //} + + + return Task.FromResult(IdentityResult.Success); + } + catch (Exception ex) + { + return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = genericIdentityErrorCode, Description = ex.Message })); + } } /// - public override Task DeleteAsync(MembersIdentityUser user, CancellationToken cancellationToken = default) + public override Task UpdateAsync(MemberIdentityUser user, CancellationToken cancellationToken = default) { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) + try { - throw new ArgumentNullException(nameof(user)); - } + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } - IMember found = _memberService.GetById(UserIdToInt(user.Id)); - if (found != null) + Attempt asInt = user.Id.TryConvertTo(); + if (asInt == false) + { + //TODO: should this be thrown, or an identity result? + throw new InvalidOperationException("The user id must be an integer to work with 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 = user.IsPropertyDirty(nameof(MemberIdentityUser.Logins)); + + if (UpdateMemberProperties(found, user)) + { + _memberService.Save(found); + } + + // TODO: when to implement external login service? + + //if (isLoginsPropertyDirty) + //{ + // _externalLoginService.Save( + // found.Id, + // user.Logins.Select(x => new ExternalLogin( + // x.LoginProvider, + // x.ProviderKey, + // x.UserData))); + //} + } + + scope.Complete(); + + return Task.FromResult(IdentityResult.Success); + } + } + catch (Exception ex) { - _memberService.Delete(found); + return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = genericIdentityErrorCode, Description = ex.Message })); } - - // 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); + public override Task DeleteAsync(MemberIdentityUser user, CancellationToken cancellationToken = default) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + IMember found = _memberService.GetById(UserIdToInt(user.Id)); + if (found != null) + { + _memberService.Delete(found); + } + + // TODO: when to implement external login service? + //_externalLoginService.DeleteUserLogins(UserIdToInt(user.Id)); + + return Task.FromResult(IdentityResult.Success); + } + catch (Exception ex) + { + return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = genericIdentityErrorCode, Description = ex.Message })); + } + } /// - protected override Task FindUserAsync(string userId, CancellationToken cancellationToken) + public override Task FindByIdAsync(string userId, CancellationToken cancellationToken = default) => FindUserAsync(userId, cancellationToken); + + /// + protected override Task FindUserAsync(string userId, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); + if (string.IsNullOrWhiteSpace(userId)) + { + throw new ArgumentNullException(nameof(userId)); + } + IMember user = _memberService.GetById(UserIdToInt(userId)); if (user == null) { - return Task.FromResult((MembersIdentityUser)null); + return Task.FromResult((MemberIdentityUser)null); } - return Task.FromResult(AssignLoginsCallback(_mapper.Map(user))); + return Task.FromResult(AssignLoginsCallback(_mapper.Map(user))); } /// - public override Task FindByNameAsync(string userName, CancellationToken cancellationToken = default) + 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); + return Task.FromResult((MemberIdentityUser)null); } - MembersIdentityUser result = AssignLoginsCallback(_mapper.Map(user)); + MemberIdentityUser result = AssignLoginsCallback(_mapper.Map(user)); return Task.FromResult(result); } /// - public override async Task SetPasswordHashAsync(MembersIdentityUser user, string passwordHash, CancellationToken cancellationToken = default) + public override async Task SetPasswordHashAsync(MemberIdentityUser 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 + // Clear this so that it's reset at the repository level + user.PasswordConfig = null; user.LastPasswordChangeDateUtc = DateTime.UtcNow; } /// - public override async Task HasPasswordAsync(MembersIdentityUser user, CancellationToken cancellationToken = default) + public override async Task HasPasswordAsync(MemberIdentityUser user, CancellationToken cancellationToken = default) { // This checks if it's null - var result = await base.HasPasswordAsync(user, cancellationToken); + bool result = await base.HasPasswordAsync(user, cancellationToken); if (result) { // we also want to check empty @@ -227,28 +258,28 @@ namespace Umbraco.Cms.Core.Security } /// - public override Task FindByEmailAsync(string email, CancellationToken cancellationToken = default) + public override Task FindByEmailAsync(string email, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); IMember member = _memberService.GetByEmail(email); - MembersIdentityUser result = member == null + MemberIdentityUser result = member == null ? null - : _mapper.Map(member); + : _mapper.Map(member); return Task.FromResult(AssignLoginsCallback(result)); } /// - public override Task GetNormalizedEmailAsync(MembersIdentityUser user, CancellationToken cancellationToken) + public override Task GetNormalizedEmailAsync(MemberIdentityUser user, CancellationToken cancellationToken) => GetEmailAsync(user, cancellationToken); /// - public override Task SetNormalizedEmailAsync(MembersIdentityUser user, string normalizedEmail, CancellationToken cancellationToken) + public override Task SetNormalizedEmailAsync(MemberIdentityUser user, string normalizedEmail, CancellationToken cancellationToken) => SetEmailAsync(user, normalizedEmail, cancellationToken); /// - public override Task AddLoginAsync(MembersIdentityUser user, UserLoginInfo login, CancellationToken cancellationToken = default) + public override Task AddLoginAsync(MemberIdentityUser user, UserLoginInfo login, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -262,8 +293,22 @@ namespace Umbraco.Cms.Core.Security throw new ArgumentNullException(nameof(login)); } + if (string.IsNullOrWhiteSpace(login.LoginProvider)) + { + throw new ArgumentNullException(nameof(login.LoginProvider)); + } + + if (string.IsNullOrWhiteSpace(login.ProviderKey)) + { + throw new ArgumentNullException(nameof(login.ProviderKey)); + } + ICollection logins = user.Logins; - var instance = new IdentityUserLogin(login.LoginProvider, login.ProviderKey, user.Id.ToString()); + var instance = new IdentityUserLogin( + login.LoginProvider, + login.ProviderKey, + user.Id.ToString()); + IdentityUserLogin userLogin = instance; logins.Add(userLogin); @@ -271,7 +316,7 @@ namespace Umbraco.Cms.Core.Security } /// - public override Task RemoveLoginAsync(MembersIdentityUser user, string loginProvider, string providerKey, CancellationToken cancellationToken = default) + public override Task RemoveLoginAsync(MemberIdentityUser user, string loginProvider, string providerKey, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -280,6 +325,16 @@ namespace Umbraco.Cms.Core.Security throw new ArgumentNullException(nameof(user)); } + if (string.IsNullOrWhiteSpace(loginProvider)) + { + throw new ArgumentNullException(nameof(loginProvider)); + } + + if (string.IsNullOrWhiteSpace(providerKey)) + { + throw new ArgumentNullException(nameof(providerKey)); + } + IIdentityUserLogin userLogin = user.Logins.SingleOrDefault(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey); if (userLogin != null) { @@ -290,7 +345,7 @@ namespace Umbraco.Cms.Core.Security } /// - public override Task> GetLoginsAsync(MembersIdentityUser user, CancellationToken cancellationToken = default) + public override Task> GetLoginsAsync(MemberIdentityUser user, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -308,24 +363,35 @@ namespace Umbraco.Cms.Core.Security cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - MembersIdentityUser user = await FindUserAsync(userId, cancellationToken); + if (string.IsNullOrWhiteSpace(loginProvider)) + { + throw new ArgumentNullException(nameof(loginProvider)); + } + + if (string.IsNullOrWhiteSpace(providerKey)) + { + throw new ArgumentNullException(nameof(providerKey)); + } + + MemberIdentityUser user = await FindUserAsync(userId, cancellationToken); if (user == null) { - return null; + return await Task.FromResult((IdentityUserLogin)null); } IList logins = await GetLoginsAsync(user, cancellationToken); UserLoginInfo found = logins.FirstOrDefault(x => x.ProviderKey == providerKey && x.LoginProvider == loginProvider); if (found == null) { - return null; + return await Task.FromResult((IdentityUserLogin)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 + // TODO: We don't store this value so it will be null + ProviderDisplayName = found.ProviderDisplayName, UserId = user.Id }; } @@ -336,9 +402,19 @@ namespace Umbraco.Cms.Core.Security cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); + if (string.IsNullOrWhiteSpace(loginProvider)) + { + throw new ArgumentNullException(nameof(loginProvider)); + } + + if (string.IsNullOrWhiteSpace(providerKey)) + { + throw new ArgumentNullException(nameof(providerKey)); + } + var logins = new List(); - // TODO: external login needed? + // TODO: external login needed //_externalLoginService.Find(loginProvider, providerKey).ToList(); if (logins.Count == 0) { @@ -350,15 +426,20 @@ namespace Umbraco.Cms.Core.Security { LoginProvider = found.LoginProvider, ProviderKey = found.ProviderKey, - ProviderDisplayName = null, // TODO: We don't store this value so it will be null + // TODO: We don't store this value so it will be null + ProviderDisplayName = null, UserId = found.UserId }); } /// - public override Task AddToRoleAsync(MembersIdentityUser user, string role, CancellationToken cancellationToken = default) + public override Task AddToRoleAsync(MemberIdentityUser user, string role, CancellationToken cancellationToken = default) { - cancellationToken.ThrowIfCancellationRequested(); + if (cancellationToken != null) + { + cancellationToken.ThrowIfCancellationRequested(); + } + ThrowIfDisposed(); if (user == null) { @@ -387,7 +468,7 @@ namespace Umbraco.Cms.Core.Security } /// - public override Task RemoveFromRoleAsync(MembersIdentityUser user, string role, CancellationToken cancellationToken = default) + public override Task RemoveFromRoleAsync(MemberIdentityUser user, string role, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); @@ -420,7 +501,7 @@ namespace Umbraco.Cms.Core.Security /// /// Gets a list of role names the specified user belongs to. /// - public override Task> GetRolesAsync(MembersIdentityUser user, CancellationToken cancellationToken = default) + public override Task> GetRolesAsync(MemberIdentityUser user, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -443,7 +524,7 @@ namespace Umbraco.Cms.Core.Security /// /// Returns true if a user is in the role /// - public override Task IsInRoleAsync(MembersIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default) + public override Task IsInRoleAsync(MemberIdentityUser user, string roleName, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -452,48 +533,59 @@ namespace Umbraco.Cms.Core.Security throw new ArgumentNullException(nameof(user)); } - return Task.FromResult(user.Roles.Select(x => x.RoleId).InvariantContains(normalizedRoleName)); + if (string.IsNullOrWhiteSpace(roleName)) + { + throw new ArgumentNullException(nameof(roleName)); + } + + return Task.FromResult(user.Roles.Select(x => x.RoleId).InvariantContains(roleName)); } /// /// Lists all users of a given role. /// - public override Task> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken = default) + public override Task> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - if (normalizedRoleName == null) + + if (string.IsNullOrWhiteSpace(roleName)) { - throw new ArgumentNullException(nameof(normalizedRoleName)); + throw new ArgumentNullException(nameof(roleName)); } - IEnumerable members = _memberService.GetMembersByMemberType(normalizedRoleName); + IEnumerable members = _memberService.GetMembersByMemberType(roleName); - IList membersIdentityUsers = members.Select(x => _mapper.Map(x)).ToList(); + IList membersIdentityUsers = members.Select(x => _mapper.Map(x)).ToList(); return Task.FromResult(membersIdentityUsers); } /// - protected override Task> FindRoleAsync(string normalizedRoleName, CancellationToken cancellationToken) + protected override Task FindRoleAsync(string roleName, CancellationToken cancellationToken) { - IMemberGroup group = _memberService.GetAllRoles().SingleOrDefault(x => x.Name == normalizedRoleName); - if (group == null) + if (string.IsNullOrWhiteSpace(roleName)) { - return Task.FromResult((IdentityRole)null); + throw new ArgumentNullException(nameof(roleName)); } - return Task.FromResult(new IdentityRole(group.Name) + IMemberGroup group = _memberService.GetAllRoles().SingleOrDefault(x => x.Name == roleName); + 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() + Id = group.Id.ToString() }); } /// protected override async Task> FindUserRoleAsync(string userId, string roleId, CancellationToken cancellationToken) { - MembersIdentityUser user = await FindUserAsync(userId, cancellationToken); + MemberIdentityUser user = await FindUserAsync(userId, cancellationToken); if (user == null) { return null; @@ -504,7 +596,7 @@ namespace Umbraco.Cms.Core.Security } /// - public override Task GetSecurityStampAsync(MembersIdentityUser user, CancellationToken cancellationToken = default) + public override Task GetSecurityStampAsync(MemberIdentityUser user, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -519,23 +611,23 @@ namespace Umbraco.Cms.Core.Security : user.SecurityStamp); } - private MembersIdentityUser AssignLoginsCallback(MembersIdentityUser user) + private MemberIdentityUser AssignLoginsCallback(MemberIdentityUser user) { if (user != null) { - //TODO: when to + //TODO: implement //user.SetLoginsCallback(new Lazy>(() => _externalLoginService.GetAll(UserIdToInt(user.Id)))); } return user; } - private bool UpdateMemberProperties(IMember member, MembersIdentityUser identityUserMember) + private bool UpdateMemberProperties(IMember member, MemberIdentityUser 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)) + if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.LastLoginDateUtc)) || (member.LastLoginDate != default && identityUserMember.LastLoginDateUtc.HasValue == false) || (identityUserMember.LastLoginDateUtc.HasValue && member.LastLoginDate.ToUniversalTime() != identityUserMember.LastLoginDateUtc.Value)) { @@ -546,7 +638,7 @@ namespace Umbraco.Cms.Core.Security member.LastLoginDate = dt; } - if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.LastPasswordChangeDateUtc)) + if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.LastPasswordChangeDateUtc)) || (member.LastPasswordChangeDate != default && identityUserMember.LastPasswordChangeDateUtc.HasValue == false) || (identityUserMember.LastPasswordChangeDateUtc.HasValue && member.LastPasswordChangeDate.ToUniversalTime() != identityUserMember.LastPasswordChangeDateUtc.Value)) { @@ -554,7 +646,7 @@ namespace Umbraco.Cms.Core.Security member.LastPasswordChangeDate = identityUserMember.LastPasswordChangeDateUtc.Value.ToLocalTime(); } - if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.EmailConfirmed)) + if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.EmailConfirmed)) || (member.EmailConfirmedDate.HasValue && member.EmailConfirmedDate.Value != default && identityUserMember.EmailConfirmed == false) || ((member.EmailConfirmedDate.HasValue == false || member.EmailConfirmedDate.Value == default) && identityUserMember.EmailConfirmed)) { @@ -562,21 +654,21 @@ namespace Umbraco.Cms.Core.Security member.EmailConfirmedDate = identityUserMember.EmailConfirmed ? (DateTime?)DateTime.Now : null; } - if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.Name)) + if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.Name)) && member.Name != identityUserMember.Name && identityUserMember.Name.IsNullOrWhiteSpace() == false) { anythingChanged = true; member.Name = identityUserMember.Name; } - if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.Email)) + if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.Email)) && member.Email != identityUserMember.Email && identityUserMember.Email.IsNullOrWhiteSpace() == false) { anythingChanged = true; member.Email = identityUserMember.Email; } - if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.AccessFailedCount)) + if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.AccessFailedCount)) && member.FailedPasswordAttempts != identityUserMember.AccessFailedCount) { anythingChanged = true; @@ -601,14 +693,14 @@ namespace Umbraco.Cms.Core.Security member.IsApproved = identityUserMember.IsApproved; } - if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.UserName)) + if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.UserName)) && member.Username != identityUserMember.UserName && identityUserMember.UserName.IsNullOrWhiteSpace() == false) { anythingChanged = true; member.Username = identityUserMember.UserName; } - if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.PasswordHash)) + if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.PasswordHash)) && member.RawPasswordValue != identityUserMember.PasswordHash && identityUserMember.PasswordHash.IsNullOrWhiteSpace() == false) { anythingChanged = true; @@ -623,7 +715,7 @@ namespace Umbraco.Cms.Core.Security } // TODO: Fix this for Groups too (as per backoffice comment) - if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.Roles)) || identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.Groups))) + if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.Roles)) || identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.Groups))) { } @@ -651,42 +743,42 @@ namespace Umbraco.Cms.Core.Security /// /// [EditorBrowsable(EditorBrowsableState.Never)] - public override Task> GetClaimsAsync(MembersIdentityUser user, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public override Task> GetClaimsAsync(MemberIdentityUser 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(); + public override Task AddClaimsAsync(MemberIdentityUser 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(); + public override Task ReplaceClaimAsync(MemberIdentityUser 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(); + public override Task RemoveClaimsAsync(MemberIdentityUser 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(); + public override Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default) => throw new NotImplementedException(); /// /// Not supported in Umbraco /// /// [EditorBrowsable(EditorBrowsableState.Never)] - protected override Task> FindTokenAsync(MembersIdentityUser user, string loginProvider, string name, CancellationToken cancellationToken) => throw new NotImplementedException(); + protected override Task> FindTokenAsync(MemberIdentityUser user, string loginProvider, string name, CancellationToken cancellationToken) => throw new NotImplementedException(); /// /// Not supported in Umbraco diff --git a/src/Umbraco.Infrastructure/Security/UmbracoIdentityRole.cs b/src/Umbraco.Infrastructure/Security/UmbracoIdentityRole.cs new file mode 100644 index 0000000000..9d06dcd037 --- /dev/null +++ b/src/Umbraco.Infrastructure/Security/UmbracoIdentityRole.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using System.ComponentModel; +using Microsoft.AspNetCore.Identity; +using Umbraco.Cms.Core.Models.Entities; + +namespace Umbraco.Cms.Core.Models.Identity +{ + public class UmbracoIdentityRole : IdentityRole, IRememberBeingDirty + { + private string _id; + private string _name; + + public event PropertyChangedEventHandler PropertyChanged + { + add + { + BeingDirty.PropertyChanged += value; + } + + remove + { + BeingDirty.PropertyChanged -= value; + } + } + + /// + public override string Id + { + get => _id; + set + { + _id = value; + HasIdentity = true; + } + } + + /// + public override string Name + { + get => _name; + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); + } + + /// + public override string NormalizedName { get => base.Name; set => base.Name = value; } + + /// + /// Gets or sets a value indicating whether returns 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; protected set; } + + // TODO: We should support this and it's logic + public override string ConcurrencyStamp { get => base.ConcurrencyStamp; set => base.ConcurrencyStamp = value; } + + /// + /// Gets the for change tracking + /// + protected BeingDirty BeingDirty { get; } = new BeingDirty(); + + /// + public bool IsDirty() => BeingDirty.IsDirty(); + + /// + public bool IsPropertyDirty(string propName) => BeingDirty.IsPropertyDirty(propName); + + /// + public IEnumerable GetDirtyProperties() => BeingDirty.GetDirtyProperties(); + + /// + public void ResetDirtyProperties() => BeingDirty.ResetDirtyProperties(); + + /// + 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(); + + /// + /// Disables change tracking. + /// + public void DisableChangeTracking() => BeingDirty.DisableChangeTracking(); + + /// + /// Enables change tracking. + /// + public void EnableChangeTracking() => BeingDirty.EnableChangeTracking(); + } +} diff --git a/src/Umbraco.Infrastructure/Security/UmbracoIdentityUser.cs b/src/Umbraco.Infrastructure/Security/UmbracoIdentityUser.cs index 525e7f839a..bf553b3d30 100644 --- a/src/Umbraco.Infrastructure/Security/UmbracoIdentityUser.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoIdentityUser.cs @@ -8,6 +8,7 @@ using Umbraco.Cms.Core.Models.Entities; namespace Umbraco.Cms.Core.Models.Identity { + /// /// Abstract class for use in Umbraco Identity for users and members /// diff --git a/src/Umbraco.Infrastructure/Services/Implement/MemberGroupService.cs b/src/Umbraco.Infrastructure/Services/Implement/MemberGroupService.cs index 5e6138980a..9024e3c4e1 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MemberGroupService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/MemberGroupService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; diff --git a/src/Umbraco.Tests.Common/Builders/Extensions/BuilderExtensions.cs b/src/Umbraco.Tests.Common/Builders/Extensions/BuilderExtensions.cs index b563cc3ec4..872a6ac367 100644 --- a/src/Umbraco.Tests.Common/Builders/Extensions/BuilderExtensions.cs +++ b/src/Umbraco.Tests.Common/Builders/Extensions/BuilderExtensions.cs @@ -17,6 +17,13 @@ namespace Umbraco.Cms.Tests.Common.Builders.Extensions return builder; } + public static T WithId(this T builder, TId id) + where T : IWithIdBuilder + { + builder.Id = id; + return builder; + } + public static T WithoutIdentity(this T builder) where T : IWithIdBuilder { diff --git a/src/Umbraco.Tests.Common/Builders/Interfaces/IWithIdBuilder.cs b/src/Umbraco.Tests.Common/Builders/Interfaces/IWithIdBuilder.cs index fe26c89d85..604f683dd7 100644 --- a/src/Umbraco.Tests.Common/Builders/Interfaces/IWithIdBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/Interfaces/IWithIdBuilder.cs @@ -7,4 +7,9 @@ namespace Umbraco.Cms.Tests.Common.Builders.Interfaces { int? Id { get; set; } } + + public interface IWithIdBuilder + { + TId Id { get; set; } + } } diff --git a/src/Umbraco.Tests.Common/Builders/UmbracoIdentityRoleBuilder.cs b/src/Umbraco.Tests.Common/Builders/UmbracoIdentityRoleBuilder.cs new file mode 100644 index 0000000000..6ffe4fd5c5 --- /dev/null +++ b/src/Umbraco.Tests.Common/Builders/UmbracoIdentityRoleBuilder.cs @@ -0,0 +1,47 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Umbraco.Cms.Core.Models.Identity; +using Umbraco.Cms.Tests.Common.Builders.Interfaces; + +namespace Umbraco.Cms.Tests.Common.Builders +{ + public class UmbracoIdentityRoleBuilder : BuilderBase, + IWithIdBuilder, + IWithNameBuilder + { + private string _id; + private string _name; + + public UmbracoIdentityRoleBuilder WithTestName(string id) + { + _name = "testname"; + _id = id; + return this; + } + + string IWithNameBuilder.Name + { + get => _name; + set => _name = value; + } + + string IWithIdBuilder.Id + { + get => _id; + set => _id = value; + } + + public override UmbracoIdentityRole Build() + { + var id = _id; + var name = _name; + + return new UmbracoIdentityRole + { + Id = id, + Name = name, + }; + } + } +} diff --git a/src/Umbraco.Tests.Common/Builders/UserBuilder.cs b/src/Umbraco.Tests.Common/Builders/UserBuilder.cs index 95fbc3a435..9d00962a9f 100644 --- a/src/Umbraco.Tests.Common/Builders/UserBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/UserBuilder.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Tests.Common.Builders.Extensions; using Umbraco.Cms.Tests.Common.Builders.Interfaces; @@ -12,6 +13,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Tests.Common.Builders { + public class UserBuilder : UserBuilder { public UserBuilder() diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.Common/MembersServiceCollectionExtensionsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.Common/MembersServiceCollectionExtensionsTests.cs index c426e26750..e76716c152 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Web.Common/MembersServiceCollectionExtensionsTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.Common/MembersServiceCollectionExtensionsTests.cs @@ -16,10 +16,10 @@ namespace Umbraco.Tests.Integration.Umbraco.Web.Common [Test] public void AddMembersIdentity_ExpectMembersUserStoreResolvable() { - IUserStore userStore = Services.GetService>(); + IUserStore userStore = Services.GetService>(); Assert.IsNotNull(userStore); - Assert.AreEqual(typeof(MembersUserStore), userStore.GetType()); + Assert.AreEqual(typeof(MemberUserStore), userStore.GetType()); } [Test] diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserManagerTests.cs index cedb8ba684..ffa778765c 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserManagerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserManagerTests.cs @@ -19,36 +19,36 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security [TestFixture] public class MemberIdentityUserManagerTests { - 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 MemberManager CreateSut() { - _mockMemberStore = new Mock>(); - _mockIdentityOptions = new Mock>(); + _mockMemberStore = new Mock>(); + _mockIdentityOptions = new Mock>(); - var idOptions = new MembersIdentityOptions { Lockout = { AllowedForNewUsers = false } }; + var idOptions = new MemberIdentityOptions { 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() @@ -56,9 +56,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security }); - var pwdValidators = new List> + var pwdValidators = new List> { - new PasswordValidator() + new PasswordValidator() }; var userManager = new MemberManager( @@ -70,12 +70,12 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security pwdValidators, new BackOfficeIdentityErrorDescriber(), _mockServiceProviders.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; @@ -86,7 +86,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security { //arrange MemberManager sut = CreateSut(); - MembersIdentityUser fakeUser = new MembersIdentityUser() + MemberIdentityUser fakeUser = new MemberIdentityUser() { PasswordConfig = "testConfig" }; @@ -147,7 +147,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security { //arrange MemberManager sut = CreateSut(); - MembersIdentityUser fakeUser = new MembersIdentityUser() + MemberIdentityUser fakeUser = new MemberIdentityUser() { PasswordConfig = "testConfig" }; diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs deleted file mode 100644 index 1a4f05d984..0000000000 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core.Mapping; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Scoping; -using Umbraco.Cms.Core.Security; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Tests.UnitTests.Umbraco.Core.ShortStringHelper; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security -{ - [TestFixture] - public class MemberIdentityUserStoreTests - { - private Mock _mockMemberService; - - public MembersUserStore CreateSut() - { - _mockMemberService = new Mock(); - return new MembersUserStore( - _mockMemberService.Object, - new UmbracoMapper(new MapDefinitionCollection(new List())), - new Mock().Object, - new IdentityErrorDescriber()); - } - - [Test] - public void GivenICreateUser_AndTheUserIsNull_ThenIShouldGetAFailedResultAsync() - { - // arrange - MembersUserStore sut = CreateSut(); - CancellationToken fakeCancellationToken = new CancellationToken(){}; - - // act - Action actual = () => sut.CreateAsync(null, fakeCancellationToken); - - // assert - Assert.That(actual, Throws.ArgumentNullException); - } - - - [Test] - public async Task GivenICreateANewUser_AndTheUserIsPopulatedCorrectly_ThenIShouldGetASuccessResultAsync() - { - // arrange - MembersUserStore sut = CreateSut(); - var fakeUser = new MembersIdentityUser() { }; - var fakeCancellationToken = new CancellationToken() { }; - - IMemberType fakeMemberType = new MemberType(new MockShortStringHelper(), 77); - IMember 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); - Assert.IsTrue(!identityResult.Errors.Any()); - } - - //GetPasswordHashAsync - //GetUserIdAsync - } -} diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs new file mode 100644 index 0000000000..15f4b7f30d --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs @@ -0,0 +1,400 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Identity; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security +{ + [TestFixture] + public class MemberRoleStoreTests + { + private Mock _mockMemberGroupService; + private IdentityErrorDescriber ErrorDescriber => new IdentityErrorDescriber(); + private UmbracoIdentityRoleBuilder _roleBuilder; + private MemberGroupBuilder _groupBuilder; + + public MemberRoleStore CreateSut() + { + _mockMemberGroupService = new Mock(); + return new MemberRoleStore( + _mockMemberGroupService.Object, + ErrorDescriber); + } + + [SetUp] + public void SetUp() + { + _roleBuilder = new UmbracoIdentityRoleBuilder(); + _groupBuilder = new MemberGroupBuilder(); + } + + [Test] + public void GivenICreateAMemberRole_AndTheGroupIsNull_ThenIShouldGetAFailedIdentityResult() + { + // arrange + MemberRoleStore sut = CreateSut(); + var fakeCancellationToken = new CancellationToken(); + + // act + Action actual = () => sut.CreateAsync(null, fakeCancellationToken); + + // assert + Assert.That(actual, Throws.ArgumentNullException); + _mockMemberGroupService.VerifyNoOtherCalls(); + } + + [Test] + public async Task GivenICreateAMemberRole_AndTheGroupIsPopulatedCorrectly_ThenIShouldGetASuccessResultAsync() + { + // arrange + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithTestName("777").Build(); + var fakeCancellationToken = new CancellationToken() { }; + + IMemberGroup mockMemberGroup = Mock.Of(m => + m.Name == "fakeGroupName" && m.CreatorId == 77); + + bool raiseEvents = false; + + _mockMemberGroupService.Setup(x => x.Save(mockMemberGroup, raiseEvents)); + + // act + IdentityResult identityResult = await sut.CreateAsync(fakeRole, fakeCancellationToken); + + // assert + Assert.IsTrue(identityResult.Succeeded); + Assert.IsTrue(!identityResult.Errors.Any()); + _mockMemberGroupService.Verify(x => x.Save(It.IsAny(), It.IsAny())); + } + + [Test] + public async Task GivenIUpdateAMemberRole_AndTheGroupExistsWithTheSameName_ThenIShouldGetASuccessResultAsyncButNoUpdatesMade() + { + // arrange + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithName("fakeGroupName").WithId("777").Build(); + var fakeCancellationToken = new CancellationToken() { }; + + IMemberGroup mockMemberGroup = Mock.Of(m => + m.Name == "fakeGroupName" && m.CreatorId == 777); + + bool raiseEvents = false; + + _mockMemberGroupService.Setup(x => x.GetById(777)).Returns(mockMemberGroup); + _mockMemberGroupService.Setup(x => x.Save(mockMemberGroup, raiseEvents)); + + // act + IdentityResult identityResult = await sut.UpdateAsync(fakeRole, fakeCancellationToken); + + // assert + Assert.IsTrue(identityResult.Succeeded); + Assert.IsTrue(!identityResult.Errors.Any()); + _mockMemberGroupService.Verify(x => x.GetById(777)); + } + + [Test] + public async Task GivenIUpdateAMemberRole_AndTheGroupExistsWithADifferentSameName_ThenIShouldGetASuccessResultAsyncWithUpdatesMade() + { + // arrange + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithName("fakeGroup777").WithId("777").Build(); + var fakeCancellationToken = new CancellationToken() { }; + + IMemberGroup mockMemberGroup = Mock.Of(m => + m.Name == "fakeGroupName" && m.CreatorId == 777); + + bool raiseEvents = false; + + _mockMemberGroupService.Setup(x => x.GetById(777)).Returns(mockMemberGroup); + _mockMemberGroupService.Setup(x => x.Save(mockMemberGroup, raiseEvents)); + + // act + IdentityResult identityResult = await sut.UpdateAsync(fakeRole, fakeCancellationToken); + + // assert + Assert.IsTrue(identityResult.Succeeded); + Assert.IsTrue(!identityResult.Errors.Any()); + _mockMemberGroupService.Verify(x => x.Save(It.IsAny(), It.IsAny())); + _mockMemberGroupService.Verify(x => x.GetById(777)); + } + + [Test] + public async Task GivenIUpdateAMemberRole_AndTheGroupDoesntExist_ThenIShouldGetAFailureResultAsync() + { + // arrange + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithTestName("777").Build(); + var fakeCancellationToken = new CancellationToken() { }; + + // act + IdentityResult identityResult = await sut.UpdateAsync(fakeRole, fakeCancellationToken); + + // assert + Assert.IsTrue(identityResult.Succeeded == false); + Assert.IsTrue(identityResult.Errors.Any(x => x.Code == "IdentityMemberGroupNotFound" && x.Description == "Member group not found")); + _mockMemberGroupService.Verify(x => x.GetById(777)); + } + + [Test] + public async Task GivenIUpdateAMemberRole_AndTheIdCannotBeParsedToAnInt_ThenIShouldGetAFailureResultAsync() + { + // arrange + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithTestName("7a77").Build(); + var fakeCancellationToken = new CancellationToken() { }; + + // act + IdentityResult identityResult = await sut.UpdateAsync(fakeRole, fakeCancellationToken); + + // assert + Assert.IsTrue(identityResult.Succeeded == false); + Assert.IsTrue(identityResult.Errors.Any(x => x.Code == "IdentityIdParseError" && x.Description == "Cannot parse ID to int")); + _mockMemberGroupService.VerifyNoOtherCalls(); + } + + [Test] + public void GivenIUpdateAMemberRole_AndTheRoleIsNull_ThenAnExceptionShouldBeThrown() + { + // arrange + MemberRoleStore sut = CreateSut(); + var fakeCancellationToken = new CancellationToken() { }; + + // act + Action actual = () => sut.UpdateAsync(null, fakeCancellationToken); + + // assert + Assert.That(actual, Throws.ArgumentNullException); + _mockMemberGroupService.VerifyNoOtherCalls(); + } + + [Test] + public async Task GivenIDeleteAMemberRole_AndItExists_ThenTheMemberGroupShouldBeDeleted_AndIShouldGetASuccessResultAsync() + { + // arrange + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithTestName("777").Build(); + var fakeCancellationToken = new CancellationToken() { }; + + IMemberGroup mockMemberGroup = Mock.Of(m => + m.Name == "fakeGroupName" && m.CreatorId == 77); + + _mockMemberGroupService.Setup(x => x.GetById(777)).Returns(mockMemberGroup); + + // act + IdentityResult identityResult = await sut.DeleteAsync(fakeRole, fakeCancellationToken); + + // assert + Assert.IsTrue(identityResult.Succeeded); + Assert.IsTrue(!identityResult.Errors.Any()); + _mockMemberGroupService.Verify(x => x.GetById(777)); + _mockMemberGroupService.Verify(x => x.Delete(mockMemberGroup)); + _mockMemberGroupService.VerifyNoOtherCalls(); + } + + [Test] + public async Task GivenIDeleteAMemberRole_AndTheIdCannotBeParsedToAnInt_ThenTheMemberGroupShouldNotBeDeleted_AndIShouldGetAnArgumentException() + { + // arrange + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithTestName("7a77").Build(); + var fakeCancellationToken = new CancellationToken() { }; + + IMemberGroup mockMemberGroup = Mock.Of(m => + m.Name == "fakeGroupName" && m.CreatorId == 77); + + + // act + Assert.ThrowsAsync(async () => await sut.DeleteAsync(fakeRole, fakeCancellationToken)); + } + + + [Test] + public async Task GivenIDeleteAMemberRole_AndItDoesntExist_ThenTheMemberGroupShouldNotBeDeleted_AndIShouldGetAFailResultAsync() + { + // arrange + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithTestName("777").Build(); + var fakeCancellationToken = new CancellationToken() { }; + + IMemberGroup mockMemberGroup = Mock.Of(m => + m.Name == "fakeGroupName" && m.CreatorId == 77); + + + // act + IdentityResult identityResult = await sut.DeleteAsync(fakeRole, fakeCancellationToken); + + // assert + Assert.IsTrue(identityResult.Succeeded == false); + Assert.IsTrue(identityResult.Errors.Any(x => x.Code == "IdentityMemberGroupNotFound" && x.Description == "Member group not found")); + _mockMemberGroupService.Verify(x => x.GetById(777)); + _mockMemberGroupService.VerifyNoOtherCalls(); + } + + [Test] + public async Task GivenIFindAMemberRoleByRoleKey_AndRoleKeyExists_ThenIShouldGetASuccessResultAsync() + { + // arrange + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithName("fakeGroupName").WithId("777").Build(); + int fakeRoleId = 777; + + IMemberGroup fakeMemberGroup = _groupBuilder.WithName("fakeGroupName").WithCreatorId(123).WithId(777).WithKey(Guid.NewGuid()).Build(); + + _mockMemberGroupService.Setup(x => x.GetById(fakeRoleId)).Returns(fakeMemberGroup); + + // act + IdentityRole actual = await sut.FindByIdAsync(fakeRole.Id); + + // assert + Assert.AreEqual(fakeRole.Name, actual.Name); + Assert.AreEqual(fakeRole.Id, actual.Id); + _mockMemberGroupService.Verify(x => x.GetById(fakeRoleId)); + _mockMemberGroupService.VerifyNoOtherCalls(); + } + + [Test] + public async Task GivenIFindAMemberRoleByRoleId_AndIdCannotBeParsedToAnIntOrGuid_ThenIShouldGetAFailureResultAsync() + { + // arrange + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithTestName("7a77").Build(); + var fakeCancellationToken = new CancellationToken() { }; + + // act + Action actual = () => sut.FindByIdAsync(fakeRole.Id, fakeCancellationToken); + + // assert + Assert.That(actual, Throws.TypeOf()); + _mockMemberGroupService.VerifyNoOtherCalls(); + } + + [Test] + public async Task GivenIFindAMemberRoleByRoleId_AndIdCannotBeParsedToAnIntButCanBeToGuid_ThenIShouldGetASuccessResultAsync() + { + // arrange + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithName("fakeGroupName").WithId("777").Build(); + + var fakeRoleGuid = Guid.NewGuid(); + + IMemberGroup fakeMemberGroup = _groupBuilder.WithName("fakeGroupName").WithCreatorId(123).WithId(777).WithKey(fakeRoleGuid).Build(); + + _mockMemberGroupService.Setup(x => x.GetById(fakeRoleGuid)).Returns(fakeMemberGroup); + + // act + IdentityRole actual = await sut.FindByIdAsync(fakeRoleGuid.ToString()); + + // assert + Assert.AreEqual(fakeRole.Name, actual.Name); + Assert.AreEqual(fakeRole.Id, actual.Id); + _mockMemberGroupService.Verify(x => x.GetById(fakeRoleGuid)); + _mockMemberGroupService.VerifyNoOtherCalls(); + } + + + [Test] + public async Task GivenIFindAMemberRoleByRoleId_AndIdCannotBeParsedToAGuidButCanBeToInt_ThenIShouldGetASuccessResultAsync() + { + // arrange + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithName("fakeGroupName").WithId("777").Build(); + + var fakeRoleId = 777; + + IMemberGroup fakeMemberGroup = _groupBuilder.WithName("fakeGroupName").WithCreatorId(123).WithId(777).WithKey(Guid.NewGuid()).Build(); + + _mockMemberGroupService.Setup(x => x.GetById(fakeRoleId)).Returns(fakeMemberGroup); + + // act + IdentityRole actual = await sut.FindByIdAsync(fakeRoleId.ToString()); + + // assert + Assert.AreEqual(fakeRole.Name, actual.Name); + Assert.AreEqual(fakeRole.Id, actual.Id); + _mockMemberGroupService.Verify(x => x.GetById(fakeRoleId)); + _mockMemberGroupService.VerifyNoOtherCalls(); + } + + + [Test] + public async Task GivenIFindAMemberRoleByRoleName_AndRoleNameExists_ThenIShouldGetASuccessResultAsync() + { + // arrange + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithName("fakeGroupName").WithId("777").Build(); + + IMemberGroup mockMemberGroup = Mock.Of(m => + m.Name == "fakeGroupName" && + m.CreatorId == 123 && + m.Id == 777); + + + _mockMemberGroupService.Setup(x => x.GetByName(fakeRole.Name)).Returns(mockMemberGroup); + + // act + IdentityRole actual = await sut.FindByNameAsync(fakeRole.Name); + + // assert + Assert.AreEqual(fakeRole.Name, actual.Name); + Assert.AreEqual(fakeRole.Id, actual.Id); + _mockMemberGroupService.Verify(x => x.GetByName(fakeRole.Name)); + } + + [Test] + public void GivenIFindAMemberRoleByRoleName_AndTheNameIsNull_ThenIShouldGetAnArgumentException() + { + // arrange + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithId("777").Build(); + var fakeCancellationToken = new CancellationToken() { }; + + // act + Action actual = () => sut.FindByNameAsync(fakeRole.Name, fakeCancellationToken); + + // assert + Assert.That(actual, Throws.ArgumentNullException); + _mockMemberGroupService.VerifyNoOtherCalls(); + + } + + [Test] + public void GivenIGetAMemberRoleId_AndTheRoleIsNull_ThenIShouldGetAnArgumentException() + { + // arrange + MemberRoleStore sut = CreateSut(); + var fakeCancellationToken = new CancellationToken() { }; + + // act + Action actual = () => sut.GetRoleIdAsync(null, fakeCancellationToken); + + // assert + Assert.That(actual, Throws.ArgumentNullException); + } + + [Test] + public void GivenIGetAMemberRoleId_AndTheRoleIsNotNull_ThenIShouldGetTheMemberRole() + { + // arrange + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithName("fakeGroupName").WithId("777").Build(); + string fakeRoleId = fakeRole.Id; + + var fakeCancellationToken = new CancellationToken(); + + // act + Task actual = sut.GetRoleIdAsync(fakeRole, fakeCancellationToken); + + // assert + Assert.AreEqual(fakeRoleId, actual.Result); + } + } +} diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs new file mode 100644 index 0000000000..7f80ff9382 --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.UnitTests.Umbraco.Core.ShortStringHelper; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security +{ + [TestFixture] + public class MemberUserStoreTests + { + private Mock _mockMemberService; + + public MemberUserStore CreateSut() + { + _mockMemberService = new Mock(); + return new MemberUserStore( + _mockMemberService.Object, + new UmbracoMapper(new MapDefinitionCollection(new List())), + new Mock().Object, + new IdentityErrorDescriber()); + } + + [Test] + public void GivenIGetNormalizedUserName_AndTheUserIsNull_ThenIShouldGetAnException() + { + // arrange + MemberUserStore sut = CreateSut(); + CancellationToken fakeCancellationToken = new CancellationToken() { }; + + // act + Action actual = () => sut.GetNormalizedUserNameAsync(null, fakeCancellationToken); + + // assert + Assert.That(actual, Throws.ArgumentNullException); + } + + [Test] + public async Task GivenIGetNormalizedUserName_AndTheEverythingIsPopulatedCorrectly_ThenIShouldGetACorrectUsername() + { + // arrange + MemberUserStore sut = CreateSut(); + var fakeUser = new MemberIdentityUser() + { + UserName = "fakeuser" + }; + + // act + string actual = await sut.GetNormalizedUserNameAsync(fakeUser); + + // assert + Assert.AreEqual(actual, fakeUser.UserName); + } + + [Test] + public void GivenISetNormalizedUserName_AndTheUserIsNull_ThenIShouldGetAnException() + { + // arrange + MemberUserStore sut = CreateSut(); + var fakeCancellationToken = new CancellationToken() { }; + + // act + Action actual = () => sut.SetNormalizedUserNameAsync(null, "username", fakeCancellationToken); + + // assert + Assert.That(actual, Throws.ArgumentNullException); + _mockMemberService.VerifyNoOtherCalls(); + } + + + [Test] + public void GivenISetNormalizedUserName_AndTheUserNameIsNull_ThenAnExceptionShouldBeThrown() + { + // arrange + MemberUserStore sut = CreateSut(); + CancellationToken fakeCancellationToken = new CancellationToken() { }; + var fakeUser = new MemberIdentityUser() { }; + + // act + Action actual = () => sut.SetNormalizedUserNameAsync(fakeUser, null, fakeCancellationToken); + + // assert + _mockMemberService.VerifyNoOtherCalls(); + } + + [Test] + public void GivenISetNormalizedUserName_AndEverythingIsPopulated_ThenIShouldGetASuccessResult() + { + // arrange + MemberUserStore sut = CreateSut(); + CancellationToken fakeCancellationToken = new CancellationToken() { }; + var fakeUser = new MemberIdentityUser() + { + UserName = "MyName" + }; + + // act + Task actual = sut.SetNormalizedUserNameAsync(fakeUser, "NewName", fakeCancellationToken); + + // assert + Assert.IsTrue(actual.IsCompletedSuccessfully); + } + + [Test] + public async Task GivenICreateUser_AndTheUserIsNull_ThenIShouldGetAFailedResultAsync() + { + // arrange + MemberUserStore sut = CreateSut(); + + // act + IdentityResult actual = await sut.CreateAsync(null); + + // assert + Assert.IsFalse(actual.Succeeded); + Assert.IsTrue(actual.Errors.Any(x => x.Code == "IdentityErrorUserStore" && x.Description == "Value cannot be null. (Parameter 'user')")); + _mockMemberService.VerifyNoOtherCalls(); + } + + [Test] + public async Task GivenICreateUser_AndTheUserDoesNotHaveIdentity_ThenIShouldGetAFailedResultAsync() + { + // arrange + MemberUserStore sut = CreateSut(); + var fakeUser = new MemberIdentityUser() { }; + var fakeCancellationToken = new CancellationToken() { }; + + IMemberType fakeMemberType = new MemberType(new MockShortStringHelper(), 77); + IMember mockMember = Mock.Of(m => + m.Name == "fakeName" && + m.Email == "fakeemail@umbraco.com" && + m.Username == "fakeUsername" && + m.RawPasswordValue == "fakePassword" && + m.ContentTypeAlias == fakeMemberType.Alias && + m.HasIdentity == false); + + _mockMemberService.Setup(x => x.CreateMember(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(mockMember); + _mockMemberService.Setup(x => x.Save(mockMember, It.IsAny())); + + // act + IdentityResult actual = await sut.CreateAsync(null); + + // assert + Assert.IsFalse(actual.Succeeded); + Assert.IsTrue(actual.Errors.Any(x => x.Code == "IdentityErrorUserStore" && x.Description == "Value cannot be null. (Parameter 'user')")); + _mockMemberService.VerifyNoOtherCalls(); + } + + [Test] + public async Task GivenICreateANewUser_AndTheUserIsPopulatedCorrectly_ThenIShouldGetASuccessResultAsync() + { + // arrange + MemberUserStore sut = CreateSut(); + var fakeUser = new MemberIdentityUser() { }; + var fakeCancellationToken = new CancellationToken() { }; + + IMemberType fakeMemberType = new MemberType(new MockShortStringHelper(), 77); + IMember 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); + Assert.IsTrue(!identityResult.Errors.Any()); + _mockMemberService.Verify(x => x.CreateMember(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); + _mockMemberService.Verify(x => x.Save(mockMember, It.IsAny())); + } + + + [Test] + public async Task GivenIDeleteUser_AndTheUserIsNotPresent_ThenIShouldGetAFailedResultAsync() + { + // arrange + MemberUserStore sut = CreateSut(); + + // act + IdentityResult actual = await sut.DeleteAsync(null); + + // assert + Assert.IsTrue(actual.Succeeded == false); + Assert.IsTrue(actual.Errors.Any(x => x.Code == "IdentityErrorUserStore" && x.Description == "Value cannot be null. (Parameter 'user')")); + _mockMemberService.VerifyNoOtherCalls(); + } + + [Test] + public async Task GivenIDeleteUser_AndTheUserIsDeletedCorrectly_ThenIShouldGetASuccessResultAsync() + { + // arrange + MemberUserStore sut = CreateSut(); + var fakeUser = new MemberIdentityUser(777); + var fakeCancellationToken = new CancellationToken() { }; + + IMemberType fakeMemberType = new MemberType(new MockShortStringHelper(), 77); + IMember mockMember = new Member(fakeMemberType) + { + Id = 777, + Name = "fakeName", + Email = "fakeemail@umbraco.com", + Username = "fakeUsername", + RawPasswordValue = "fakePassword" + }; + + _mockMemberService.Setup(x => x.GetById(mockMember.Id)).Returns(mockMember); + _mockMemberService.Setup(x => x.Delete(mockMember)); + + // act + IdentityResult identityResult = await sut.DeleteAsync(fakeUser, fakeCancellationToken); + + // assert + Assert.IsTrue(identityResult.Succeeded); + Assert.IsTrue(!identityResult.Errors.Any()); + _mockMemberService.Verify(x => x.GetById(mockMember.Id)); + _mockMemberService.Verify(x => x.Delete(mockMember)); + _mockMemberService.VerifyNoOtherCalls(); + } + } +} 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 63d587bb94..321f34f517 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs @@ -19,6 +19,7 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.ContentApps; using Umbraco.Cms.Core.Dictionary; using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; @@ -38,7 +39,6 @@ using Umbraco.Cms.Web.BackOffice.Controllers; using Umbraco.Cms.Web.BackOffice.Mapping; using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Security; -using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; using MemberMapDefinition = Umbraco.Cms.Web.BackOffice.Mapping.MemberMapDefinition; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers @@ -70,7 +70,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers IMemberGroupService memberGroupService, IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - IPasswordChanger passwordChanger, + IPasswordChanger passwordChanger, IOptions globalSettings, IUser user) { @@ -80,14 +80,14 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers sut.ModelState.AddModelError("key", "Invalid model state"); Mock.Get(umbracoMembersUserManager) - .Setup(x => x.CreateAsync(It.IsAny(), 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); var value = new MemberDisplay(); - + // act ActionResult result = sut.PostSave(fakeMemberData).Result; var validation = result.Result as ValidationErrorResult; @@ -108,14 +108,14 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IBackOfficeSecurity backOfficeSecurity, - IPasswordChanger passwordChanger, + IPasswordChanger passwordChanger, IOptions globalSettings, IUser user) { // arrange Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew); Mock.Get(umbracoMembersUserManager) - .Setup(x => x.CreateAsync(It.IsAny(), It.IsAny())) + .Setup(x => x.CreateAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(() => IdentityResult.Success); Mock.Get(umbracoMembersUserManager) .Setup(x => x.ValidatePasswordAsync(It.IsAny())) @@ -149,14 +149,14 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IBackOfficeSecurity backOfficeSecurity, - IPasswordChanger passwordChanger, + IPasswordChanger passwordChanger, IOptions globalSettings, IUser user) { // arrange Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew); Mock.Get(umbracoMembersUserManager) - .Setup(x => x.CreateAsync(It.IsAny(), It.IsAny())) + .Setup(x => x.CreateAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(() => IdentityResult.Success); Mock.Get(umbracoMembersUserManager) .Setup(x => x.ValidatePasswordAsync(It.IsAny())) @@ -191,13 +191,13 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IBackOfficeSecurity backOfficeSecurity, - IPasswordChanger passwordChanger, + IPasswordChanger passwordChanger, IOptions globalSettings, IUser user) { // arrange Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.Save); - var membersIdentityUser = new MembersIdentityUser(123); + var membersIdentityUser = new MemberIdentityUser(123); Mock.Get(umbracoMembersUserManager) .Setup(x => x.FindByIdAsync(It.IsAny())) .ReturnsAsync(() => membersIdentityUser); @@ -206,7 +206,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers .ReturnsAsync(() => IdentityResult.Success); Mock.Get(umbracoMembersUserManager) - .Setup(x => x.UpdateAsync(It.IsAny())) + .Setup(x => x.UpdateAsync(It.IsAny())) .ReturnsAsync(() => IdentityResult.Success); Mock.Get(memberTypeService).Setup(x => x.GetDefault()).Returns("fakeAlias"); Mock.Get(globalSettings); @@ -243,13 +243,13 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IBackOfficeSecurity backOfficeSecurity, - IPasswordChanger passwordChanger, + IPasswordChanger passwordChanger, IOptions globalSettings, IUser user) { // arrange Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.Save); - var membersIdentityUser = new MembersIdentityUser(123); + var membersIdentityUser = new MemberIdentityUser(123); Mock.Get(umbracoMembersUserManager) .Setup(x => x.FindByIdAsync(It.IsAny())) .ReturnsAsync(() => membersIdentityUser); @@ -258,7 +258,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers .ReturnsAsync(() => IdentityResult.Success); Mock.Get(umbracoMembersUserManager) - .Setup(x => x.UpdateAsync(It.IsAny())) + .Setup(x => x.UpdateAsync(It.IsAny())) .ReturnsAsync(() => IdentityResult.Success); Mock.Get(memberTypeService).Setup(x => x.GetDefault()).Returns("fakeAlias"); Mock.Get(globalSettings); @@ -290,7 +290,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers Mock.Get(backOfficeSecurity).Setup(x => x.CurrentUser).Returns(user); } - private static void SetupPasswordSuccess(IMemberManager umbracoMembersUserManager, IPasswordChanger passwordChanger, bool successful = true) + private static void SetupPasswordSuccess(IMemberManager umbracoMembersUserManager, IPasswordChanger passwordChanger, bool successful = true) { var passwordChanged = new PasswordChangedModel() { @@ -323,14 +323,14 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IBackOfficeSecurity backOfficeSecurity, - IPasswordChanger passwordChanger, + IPasswordChanger passwordChanger, IOptions globalSettings, IUser user) { // arrange Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew); Mock.Get(umbracoMembersUserManager) - .Setup(x => x.CreateAsync(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); @@ -338,7 +338,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers .Setup(x => x.ValidatePasswordAsync(It.IsAny())) .ReturnsAsync(() => IdentityResult.Success); Mock.Get(umbracoMembersUserManager) - .Setup(x => x.AddToRolesAsync(It.IsAny(), It.IsAny>())) + .Setup(x => x.AddToRolesAsync(It.IsAny(), It.IsAny>())) .ReturnsAsync(() => IdentityResult.Success); Mock.Get(memberService).SetupSequence( @@ -346,7 +346,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers .Returns(() => member); MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor, passwordChanger, globalSettings, user); - + // act ActionResult result = sut.PostSave(fakeMemberData).Result; var validation = result.Result as ValidationErrorResult; @@ -367,7 +367,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IBackOfficeSecurity backOfficeSecurity, - IPasswordChanger passwordChanger, + IPasswordChanger passwordChanger, IOptions globalSettings, IUser user) { @@ -378,7 +378,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers { roleName }; - var membersIdentityUser = new MembersIdentityUser(123); + var membersIdentityUser = new MemberIdentityUser(123); Mock.Get(umbracoMembersUserManager) .Setup(x => x.FindByIdAsync(It.IsAny())) .ReturnsAsync(() => membersIdentityUser); @@ -386,10 +386,10 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers .Setup(x => x.ValidatePasswordAsync(It.IsAny())) .ReturnsAsync(() => IdentityResult.Success); Mock.Get(umbracoMembersUserManager) - .Setup(x => x.UpdateAsync(It.IsAny())) + .Setup(x => x.UpdateAsync(It.IsAny())) .ReturnsAsync(() => IdentityResult.Success); Mock.Get(umbracoMembersUserManager) - .Setup(x => x.AddToRolesAsync(It.IsAny(), It.IsAny>())) + .Setup(x => x.AddToRolesAsync(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); @@ -435,10 +435,10 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers IMemberService memberService, IMemberTypeService memberTypeService, IMemberGroupService memberGroupService, - IUmbracoUserManager membersUserManager, + IUmbracoUserManager membersUserManager, IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - IPasswordChanger passwordChanger, + IPasswordChanger passwordChanger, IOptions globalSettings, IUser user) { @@ -484,8 +484,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers memberGroupService, mockPasswordConfig.Object, contentTypeBaseServiceProvider.Object, - propertyEditorCollection), - httpContextAccessor); + propertyEditorCollection)); var map = new MapDefinitionCollection(new List() { diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index c3690b4863..9518a9b84d 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -2434,6 +2434,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers .Select(_umbracoMapper.Map) .ToArray(); + //TODO: change to role manager var allGroups = _memberGroupService.GetAll().ToArray(); var groups = entry.Rules .Where(rule => rule.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType) diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index be652860b5..567303e150 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -58,7 +58,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; private readonly IJsonSerializer _jsonSerializer; private readonly IShortStringHelper _shortStringHelper; - private readonly IPasswordChanger _passwordChanger; + private readonly IPasswordChanger _passwordChanger; private readonly IScopeProvider _scopeProvider; /// @@ -92,7 +92,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IJsonSerializer jsonSerializer, - IPasswordChanger passwordChanger, + IPasswordChanger passwordChanger, IScopeProvider scopeProvider) : base(cultureDictionary, loggerFactory, shortStringHelper, eventMessages, localizedTextService, jsonSerializer) { @@ -362,7 +362,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}"); } - var identityMember = MembersIdentityUser.CreateNew( + var identityMember = MemberIdentityUser.CreateNew( contentItem.Username, contentItem.Email, memberType.Alias, @@ -394,9 +394,6 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } } - //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); contentItem.PersistedContent = member; @@ -459,7 +456,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers bool needsResync = false; - MembersIdentityUser identityMember = await _memberManager.FindByIdAsync(contentItem.Id.ToString()); + MemberIdentityUser identityMember = await _memberManager.FindByIdAsync(contentItem.Id.ToString()); if (identityMember == null) { return new ValidationErrorResult("Member was not found"); @@ -603,7 +600,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// The groups to updates /// The member as an identity user - private async Task> AddOrUpdateRoles(IEnumerable groups, MembersIdentityUser identityMember) + private async Task> AddOrUpdateRoles(IEnumerable groups, MemberIdentityUser identityMember) { var hasChanges = false; @@ -654,7 +651,6 @@ namespace Umbraco.Cms.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.BackOffice/Controllers/MemberGroupController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs index 7146cd5820..88825c0d9a 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs @@ -30,13 +30,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers public MemberGroupController( IMemberGroupService memberGroupService, UmbracoMapper umbracoMapper, - ILocalizedTextService localizedTextService - ) + ILocalizedTextService localizedTextService) { _memberGroupService = memberGroupService ?? throw new ArgumentNullException(nameof(memberGroupService)); _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); - _localizedTextService = - localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); + _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); } /// @@ -46,17 +44,16 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// public ActionResult GetById(int id) { - var memberGroup = _memberGroupService.GetById(id); + IMemberGroup memberGroup = _memberGroupService.GetById(id); if (memberGroup == null) { return NotFound(); } - var dto = _umbracoMapper.Map(memberGroup); + MemberGroupDisplay dto = _umbracoMapper.Map(memberGroup); return dto; } - /// /// Gets the member group json for the member group guid /// @@ -64,7 +61,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// public ActionResult GetById(Guid id) { - var memberGroup = _memberGroupService.GetById(id); + IMemberGroup memberGroup = _memberGroupService.GetById(id); if (memberGroup == null) { return NotFound(); @@ -82,9 +79,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { var guidUdi = id as GuidUdi; if (guidUdi == null) + { return NotFound(); + } - var memberGroup = _memberGroupService.GetById(guidUdi.Guid); + IMemberGroup memberGroup = _memberGroupService.GetById(guidUdi.Guid); if (memberGroup == null) { return NotFound(); @@ -93,11 +92,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return _umbracoMapper.Map(memberGroup); } - public IEnumerable GetByIds([FromQuery]int[] ids) - { - return _memberGroupService.GetByIds(ids) - .Select(_umbracoMapper.Map); - } + public IEnumerable GetByIds([FromQuery] int[] ids) + => _memberGroupService.GetByIds(ids).Select(_umbracoMapper.Map); [HttpDelete] [HttpPost] @@ -114,10 +110,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } public IEnumerable GetAllGroups() - { - return _memberGroupService.GetAll() + => _memberGroupService.GetAll() .Select(_umbracoMapper.Map); - } public MemberGroupDisplay GetEmpty() { @@ -129,7 +123,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { var id = int.Parse(saveModel.Id.ToString()); - var memberGroup = id > 0 ? _memberGroupService.GetById(id) : new MemberGroup(); + IMemberGroup memberGroup = id > 0 ? _memberGroupService.GetById(id) : new MemberGroup(); if (memberGroup == null) { return NotFound(); @@ -138,7 +132,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers memberGroup.Name = saveModel.Name; _memberGroupService.Save(memberGroup); - var display = _umbracoMapper.Map(memberGroup); + MemberGroupDisplay display = _umbracoMapper.Map(memberGroup); display.AddSuccessNotification( _localizedTextService.Localize("speechBubbles/memberGroupSavedHeader"), diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index cc5f3664a9..21098ffabf 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -90,7 +90,7 @@ namespace Umbraco.Extensions builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique, PasswordChanger>(); - builder.Services.AddUnique, PasswordChanger>(); + builder.Services.AddUnique, PasswordChanger>(); builder.AddNotificationHandler(); builder.AddNotificationHandler(); diff --git a/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs index e88503794e..b5592b08ff 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs @@ -1,4 +1,6 @@ +using System; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; @@ -17,15 +19,12 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping private readonly CommonMapper _commonMapper; private readonly CommonTreeNodeMapper _commonTreeNodeMapper; private readonly MemberTabsAndPropertiesMapper _tabsAndPropertiesMapper; - private readonly IHttpContextAccessor _httpContextAccessor; - public MemberMapDefinition(CommonMapper commonMapper, CommonTreeNodeMapper commonTreeNodeMapper, MemberTabsAndPropertiesMapper tabsAndPropertiesMapper, IHttpContextAccessor httpContextAccessor) + public MemberMapDefinition(CommonMapper commonMapper, CommonTreeNodeMapper commonTreeNodeMapper, MemberTabsAndPropertiesMapper tabsAndPropertiesMapper) { _commonMapper = commonMapper; _commonTreeNodeMapper = commonTreeNodeMapper; - _tabsAndPropertiesMapper = tabsAndPropertiesMapper; - _httpContextAccessor = httpContextAccessor; } public void DefineMaps(UmbracoMapper mapper) diff --git a/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs index 7445f35f29..0559a17a53 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs @@ -28,25 +28,21 @@ namespace Umbraco.Cms.Web.BackOffice.Trees IMemberGroupService memberGroupService, IEventAggregator eventAggregator) : base(localizedTextService, umbracoApiControllerTypeCollection, menuItemCollectionFactory, eventAggregator) - { - _memberGroupService = memberGroupService; - } + => _memberGroupService = memberGroupService; protected override IEnumerable GetTreeNodesFromService(string id, FormCollection queryStrings) - { - return _memberGroupService.GetAll() + => _memberGroupService.GetAll() .OrderBy(x => x.Name) .Select(dt => CreateTreeNode(dt.Id.ToString(), id, queryStrings, dt.Name, Constants.Icons.MemberGroup, false)); - } protected override ActionResult CreateRootNode(FormCollection queryStrings) { - var rootResult = base.CreateRootNode(queryStrings); + ActionResult rootResult = base.CreateRootNode(queryStrings); if (!(rootResult.Result is null)) { return rootResult; } - var root = rootResult.Value; + TreeNode root = rootResult.Value; //check if there are any groups root.HasChildren = _memberGroupService.GetAll().Any(); diff --git a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs index 8b81fc673c..5182db4e20 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Web.Caching; using SixLabors.ImageSharp.Web.Commands; @@ -65,17 +66,19 @@ namespace Umbraco.Extensions public static void AddMembersIdentity(this IServiceCollection services) => services.BuildMembersIdentity() .AddDefaultTokenProviders() - .AddUserStore() - .AddMembersManager(); + .AddMemberManager() + .AddUserStore() + .AddRoleStore() + .AddRoleValidator>() + .AddRoleManager>(); - - private static MembersIdentityBuilder BuildMembersIdentity(this IServiceCollection services) + private static MemberIdentityBuilder BuildMembersIdentity(this IServiceCollection services) { // Services used by Umbraco members identity - services.TryAddScoped, UserValidator>(); - services.TryAddScoped, PasswordValidator>(); - services.TryAddScoped, PasswordHasher>(); - return new MembersIdentityBuilder(services); + services.TryAddScoped, UserValidator>(); + services.TryAddScoped, PasswordValidator>(); + services.TryAddScoped, PasswordHasher>(); + return new MemberIdentityBuilder(typeof(IdentityRole), services); } private static void RemoveIntParamenterIfValueGreatherThen(IDictionary commands, string parameter, int maxValue) diff --git a/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs index 4daf5457bf..6940221518 100644 --- a/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs @@ -10,13 +10,13 @@ namespace Umbraco.Extensions public static class IdentityBuilderExtensions { /// - /// Adds a for the . + /// Adds a for the . /// - /// The usermanager interface - /// The usermanager type + /// The member manager interface + /// The member manager type /// The current instance. - public static IdentityBuilder AddMembersManager(this IdentityBuilder identityBuilder) - where TUserManager : UserManager, TInterface + public static IdentityBuilder AddMemberManager(this IdentityBuilder identityBuilder) + where TUserManager : UserManager, TInterface { identityBuilder.Services.AddScoped(typeof(TInterface), typeof(TUserManager)); return identityBuilder; diff --git a/src/Umbraco.Web.Common/Security/MemberManager.cs b/src/Umbraco.Web.Common/Security/MemberManager.cs index a6165bac79..887dfa5b92 100644 --- a/src/Umbraco.Web.Common/Security/MemberManager.cs +++ b/src/Umbraco.Web.Common/Security/MemberManager.cs @@ -10,19 +10,19 @@ using Umbraco.Cms.Core.Security; namespace Umbraco.Cms.Web.Common.Security { - public class MemberManager : UmbracoUserManager, IMemberManager + public class MemberManager : UmbracoUserManager, IMemberManager { public MemberManager( IIpResolver ipResolver, - IUserStore store, - IOptions optionsAccessor, - IPasswordHasher passwordHasher, - IEnumerable> userValidators, - IEnumerable> passwordValidators, + IUserStore store, + IOptions optionsAccessor, + IPasswordHasher passwordHasher, + IEnumerable> userValidators, + IEnumerable> passwordValidators, BackOfficeIdentityErrorDescriber errors, IServiceProvider services, - ILogger> logger, + ILogger> logger, IOptions passwordConfiguration) : base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, errors, services, logger, passwordConfiguration) { diff --git a/src/Umbraco.Web/Security/Providers/MembersRoleProvider.cs b/src/Umbraco.Web/Security/Providers/MembersRoleProvider.cs deleted file mode 100644 index f28aa75e48..0000000000 --- a/src/Umbraco.Web/Security/Providers/MembersRoleProvider.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using System.Configuration.Provider; -using System.Linq; -using System.Web.Security; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Persistence.Querying; -using Umbraco.Cms.Core.Services; -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; - private string _applicationName; - - public MembersRoleProvider(IMembershipRoleService roleService) - { - _roleService = roleService; - } - - public MembersRoleProvider() - : this(Current.Services.MemberService) - { - } - - public override bool IsUserInRole(string username, string roleName) - { - return GetRolesForUser(username).Any(x => x == roleName); - } - - public override string[] GetRolesForUser(string username) - { - return _roleService.GetAllRoles(username).ToArray(); - } - - public override void CreateRole(string roleName) - { - _roleService.AddRole(roleName); - } - - public override bool DeleteRole(string roleName, bool throwOnPopulatedRole) - { - return _roleService.DeleteRole(roleName, throwOnPopulatedRole); - } - - /// - /// 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.GetAllRoles().Any(x => x.Name == roleName); - - public override void AddUsersToRoles(string[] usernames, string[] roleNames) - { - _roleService.AssignRoles(usernames, roleNames); - } - - public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames) - { - _roleService.DissociateRoles(usernames, roleNames); - } - - public override string[] GetUsersInRole(string roleName) - { - return _roleService.GetMembersInRole(roleName).Select(x => x.Username).ToArray(); - } - - /// - /// Gets all the member roles - /// - /// A list of member roles - public override string[] GetAllRoles() => _roleService.GetAllRoles().Select(x => x.Name).ToArray(); - - public override string[] FindUsersInRole(string roleName, string usernameToMatch) - { - return _roleService.FindMembersInRole(roleName, usernameToMatch, StringPropertyMatchType.Wildcard).Select(x => x.Username).ToArray(); - } - - /// - /// The name of the application using the custom role provider. - /// - /// - /// The name of the application using the custom membership provider. - public override string ApplicationName - { - get - { - return _applicationName; - } - - set - { - if (string.IsNullOrEmpty(value)) - throw new ProviderException("ApplicationName cannot be empty."); - - if (value.Length > 0x100) - throw new ProviderException("Provider application name too long."); - - _applicationName = value; - } - } - } -} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index b06e6446c2..ec1db29f99 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -178,7 +178,6 @@ -