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/MembersRoleStore.cs b/src/Umbraco.Infrastructure/Security/MembersRoleStore.cs new file mode 100644 index 0000000000..3f238c5c48 --- /dev/null +++ b/src/Umbraco.Infrastructure/Security/MembersRoleStore.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections; +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.Models; +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 MembersRoleStore : RoleStoreBase, string, IdentityUserRole, IdentityRoleClaim> + { + private readonly IMemberService _memberService; + private readonly IMemberGroupService _memberGroupService; + private readonly IScopeProvider _scopeProvider; + + public MembersRoleStore(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 + { + IEnumerable memberGroups = _memberGroupService.GetAll(); + var identityRoles = new List>(); + foreach (IMemberGroup group in memberGroups) + { + IdentityRole identityRole = MapFromMemberGroup(group); + identityRoles.Add(identityRole); + } + + return identityRoles.AsQueryable(); + } + } + + /// + public override Task CreateAsync( + IdentityRole role, + CancellationToken cancellationToken = new CancellationToken()) + { + 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 override Task UpdateAsync(IdentityRole role, + CancellationToken cancellationToken = new CancellationToken()) + { + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + + if (!int.TryParse(role.Id, out int roleId)) + { + return new Task(() => IdentityResult.Failed()); + } + + IMemberGroup memberGroup = _memberGroupService.GetById(roleId); + if (memberGroup != null) + { + if (MapToMemberGroup(role, memberGroup)) + { + _memberGroupService.Save(memberGroup); + } + } + + return Task.FromResult(IdentityResult.Success); + } + + /// + public override Task DeleteAsync(IdentityRole role, + CancellationToken cancellationToken = new CancellationToken()) + { + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + + if (!int.TryParse(role.Id, out int roleId)) + { + return new Task(() => IdentityResult.Failed()); + } + + IMemberGroup memberGroup = _memberGroupService.GetById(roleId); + if (memberGroup != null) + { + _memberGroupService.Delete(memberGroup); + } + else + { + //TODO: throw exception when not found, or return failure? + return Task.FromResult(IdentityResult.Failed()); + } + + return Task.FromResult(IdentityResult.Success); + } + + /// + public override Task> FindByIdAsync(string id, + CancellationToken cancellationToken = new CancellationToken()) + { + if (!int.TryParse(id, out int roleId)) + { + return null; + } + + IMemberGroup memberGroup = _memberGroupService.GetById(roleId); + + return Task.FromResult(memberGroup == null ? null : MapFromMemberGroup(memberGroup)); + } + + /// + public override Task> FindByNameAsync(string normalizedName, + CancellationToken cancellationToken = new CancellationToken()) + { + IMemberGroup memberGroup = _memberGroupService.GetByName(normalizedName); + + return Task.FromResult(memberGroup == null ? null : MapFromMemberGroup(memberGroup)); + } + + /// + 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(); + + /// + /// Maps a member group to an identity role + /// + /// + /// + private IdentityRole MapFromMemberGroup(IMemberGroup memberGroup) + { + var result = new IdentityRole + { + Id = memberGroup.Id.ToString(), + Name = memberGroup.Name + }; + + return result; + } + + /// + /// Map an identity role to a member group + /// + /// + /// + /// + private bool MapToMemberGroup(IdentityRole role, IMemberGroup memberGroup) + { + var anythingChanged = false; + + if (!string.IsNullOrEmpty(role.Name) && memberGroup.Name != role.Name) + { + memberGroup.Name = role.Name; + anythingChanged = true; + } + + return anythingChanged; + } + } +} diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs index 1a4f05d984..8afe7fe198 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs @@ -74,8 +74,5 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security 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..d9337f6f10 --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs @@ -0,0 +1,222 @@ +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.Models; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security +{ + [TestFixture] + public class MemberRoleStoreTests + { + private Mock _mockMemberService; + private Mock _mockMemberGroupService; + + public MembersRoleStore CreateSut() + { + _mockMemberService = new Mock(); + _mockMemberGroupService = new Mock(); + return new MembersRoleStore( + _mockMemberService.Object, + _mockMemberGroupService.Object, + new Mock().Object, + new IdentityErrorDescriber()); + } + + [Test] + public void GivenICreateAMemberRole_AndTheGroupIsNull_ThenIShouldGetAFailedResultAsync() + { + // arrange + MembersRoleStore sut = CreateSut(); + CancellationToken fakeCancellationToken = new CancellationToken() { }; + + // act + Action actual = () => sut.CreateAsync(null, fakeCancellationToken); + + // assert + Assert.That(actual, Throws.ArgumentNullException); + } + + + [Test] + public async Task GivenICreateAMemberRole_AndTheGroupIsPopulatedCorrectly_ThenIShouldGetASuccessResultAsync() + { + // arrange + MembersRoleStore sut = CreateSut(); + var fakeRole = new IdentityRole() + { + Id = "777", + Name = "testname" + }; + 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_AndTheGroupExists_ThenIShouldGetASuccessResultAsync() + { + // arrange + MembersRoleStore sut = CreateSut(); + var fakeRole = new IdentityRole() + { + Id = "777", + Name = "testname" + }; + var fakeCancellationToken = new CancellationToken() { }; + + IMemberGroup mockMemberGroup = Mock.Of(m => + m.Name == "fakeGroupName" && m.CreatorId == 77); + + 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(mockMemberGroup, false)); + _mockMemberGroupService.Verify(x => x.GetById(777)); + } + + [Test] + public async Task GivenIUpdateAMemberRole_AndTheGroupDoesntExist_ThenIShouldGetAFailureResultAsync() + { + // arrange + MembersRoleStore sut = CreateSut(); + var fakeRole = new IdentityRole() + { + Id = "777", + Name = "testname" + }; + var fakeCancellationToken = new CancellationToken() { }; + + IMemberGroup mockMemberGroup = Mock.Of(m => + m.Name == "fakeGroupName" && m.CreatorId == 77); + + bool raiseEvents = false; + + + // act + IdentityResult identityResult = await sut.UpdateAsync(fakeRole, fakeCancellationToken); + + // assert + Assert.IsTrue(identityResult.Succeeded == false); + Assert.IsTrue(identityResult.Errors.Any()); + _mockMemberGroupService.Verify(x => x.GetById(777)); + } + + [Test] + public async Task GivenIDeleteAMemberRole_AndItExists_ThenTheMemberGroupShouldBeDeleted_AndIShouldGetASuccessResultAsync() + { + // arrange + MembersRoleStore sut = CreateSut(); + var fakeRole = new IdentityRole() + { + Id = "777", + Name = "testname" + }; + 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)); + } + + [Test] + public async Task GivenIDeleteAMemberRole_AndItDoesntExist_ThenTheMemberGroupShouldBeDeleted_AndIShouldGetASuccessResultAsync() + { + // arrange + MembersRoleStore sut = CreateSut(); + var fakeRole = new IdentityRole() + { + Id = "777", + Name = "testname" + }; + 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()); + _mockMemberGroupService.Verify(x => x.GetById(777)); + _mockMemberGroupService.Verify(x => x.Delete(mockMemberGroup)); + } + + [Test] + public async Task GivenIGetAllMemberRoles_ThenIShouldGetAllMemberGroups_AndASuccessResultAsync() + { + // arrange + MembersRoleStore sut = CreateSut(); + var fakeRole = new IdentityRole() + { + Id = "777", + Name = "testname" + }; + IEnumerable> expected = new List>() + { + new IdentityRole("fakeGroupName") + { + Id = "77" + } + }; + + IMemberGroup mockMemberGroup = Mock.Of(m => + m.Name == "fakeGroupName" && m.CreatorId == 77); + + IEnumerable fakeMemberGroups = new List() + { + mockMemberGroup + }; + + _mockMemberGroupService.Setup(x => x.GetAll()).Returns(fakeMemberGroups); + + // act + IQueryable> actual = sut.Roles; + + // assert + Assert.AreEqual(expected.AsQueryable(), actual); + _mockMemberGroupService.Verify(x => x.GetAll()); + } + } +}