From c9229880da8d322e5931982dc828071b517184b9 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Wed, 3 Mar 2021 17:33:57 +0000 Subject: [PATCH 01/78] Renamed and added initial member role store methods, and initial unit tests --- .../Security/MemberRolesUserStore.cs | 57 ----- .../Security/MembersRoleStore.cs | 193 +++++++++++++++ .../Security/MemberIdentityUserStoreTests.cs | 3 - .../Security/MemberRoleStoreTests.cs | 222 ++++++++++++++++++ 4 files changed, 415 insertions(+), 60 deletions(-) delete mode 100644 src/Umbraco.Infrastructure/Security/MemberRolesUserStore.cs create mode 100644 src/Umbraco.Infrastructure/Security/MembersRoleStore.cs create mode 100644 src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs 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()); + } + } +} From 4cb9923c24bd75f004c38305e8a254e63a6d69c3 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Thu, 4 Mar 2021 13:46:12 +0000 Subject: [PATCH 02/78] Updated errors and unit tests --- .../Security/MembersRoleStore.cs | 15 ++- .../Security/MemberRoleStoreTests.cs | 117 ++++++++++++++---- 2 files changed, 107 insertions(+), 25 deletions(-) diff --git a/src/Umbraco.Infrastructure/Security/MembersRoleStore.cs b/src/Umbraco.Infrastructure/Security/MembersRoleStore.cs index 3f238c5c48..3aafa702cd 100644 --- a/src/Umbraco.Infrastructure/Security/MembersRoleStore.cs +++ b/src/Umbraco.Infrastructure/Security/MembersRoleStore.cs @@ -80,7 +80,8 @@ namespace Umbraco.Cms.Core.Security if (!int.TryParse(role.Id, out int roleId)) { - return new Task(() => IdentityResult.Failed()); + //TODO: what identity error should we return in this case? + return Task.FromResult(IdentityResult.Failed(ErrorDescriber.DefaultError())); } IMemberGroup memberGroup = _memberGroupService.GetById(roleId); @@ -91,6 +92,11 @@ namespace Umbraco.Cms.Core.Security _memberGroupService.Save(memberGroup); } } + else + { + //TODO: throw exception when not found, or return failure? And is this the correcet message + return Task.FromResult(IdentityResult.Failed(ErrorDescriber.InvalidRoleName(role.Name))); + } return Task.FromResult(IdentityResult.Success); } @@ -106,7 +112,8 @@ namespace Umbraco.Cms.Core.Security if (!int.TryParse(role.Id, out int roleId)) { - return new Task(() => IdentityResult.Failed()); + //TODO: what identity error should we return in this case? + return Task.FromResult(IdentityResult.Failed(ErrorDescriber.DefaultError())); } IMemberGroup memberGroup = _memberGroupService.GetById(roleId); @@ -116,8 +123,8 @@ namespace Umbraco.Cms.Core.Security } else { - //TODO: throw exception when not found, or return failure? - return Task.FromResult(IdentityResult.Failed()); + //TODO: throw exception when not found, or return failure? And is this the correcet message + return Task.FromResult(IdentityResult.Failed(ErrorDescriber.InvalidRoleName(role.Name))); } return Task.FromResult(IdentityResult.Success); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs index d9337f6f10..6c3c741376 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs @@ -18,6 +18,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security { private Mock _mockMemberService; private Mock _mockMemberGroupService; + private IdentityErrorDescriber ErrorDescriber => new IdentityErrorDescriber(); public MembersRoleStore CreateSut() { @@ -27,7 +28,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security _mockMemberService.Object, _mockMemberGroupService.Object, new Mock().Object, - new IdentityErrorDescriber()); + ErrorDescriber); } [Test] @@ -74,19 +75,19 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security } [Test] - public async Task GivenIUpdateAMemberRole_AndTheGroupExists_ThenIShouldGetASuccessResultAsync() + public async Task GivenIUpdateAMemberRole_AndTheGroupExistsWithTheSameName_ThenIShouldGetASuccessResultAsyncButNoUpdatesMade() { // arrange MembersRoleStore sut = CreateSut(); var fakeRole = new IdentityRole() { Id = "777", - Name = "testname" + Name = "fakeGroupName" }; var fakeCancellationToken = new CancellationToken() { }; IMemberGroup mockMemberGroup = Mock.Of(m => - m.Name == "fakeGroupName" && m.CreatorId == 77); + m.Name == "fakeGroupName" && m.CreatorId == 777); bool raiseEvents = false; @@ -99,7 +100,36 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security // 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_AndTheGroupExistsWithADifferentSameName_ThenIShouldGetASuccessResultAsyncWithUpdatesMade() + { + // arrange + MembersRoleStore sut = CreateSut(); + var fakeRole = new IdentityRole() + { + Id = "777", + Name = "fakeGroup777" + }; + 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)); } @@ -115,8 +145,29 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security }; 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(x => x.Code == "InvalidRoleName" && x.Description == "Role name 'testname' is invalid.")); + _mockMemberGroupService.Verify(x => x.GetById(777)); + } + + [Test] + public async Task GivenIUpdateAMemberRole_AndTheIdCannotBeParsedToAnInt_ThenIShouldGetAFailureResultAsync() + { + // arrange + MembersRoleStore sut = CreateSut(); + var fakeRole = new IdentityRole() + { + Id = "7a77", + Name = "testname" + }; + var fakeCancellationToken = new CancellationToken() { }; bool raiseEvents = false; @@ -126,8 +177,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security // assert Assert.IsTrue(identityResult.Succeeded == false); - Assert.IsTrue(identityResult.Errors.Any()); - _mockMemberGroupService.Verify(x => x.GetById(777)); + Assert.IsTrue(identityResult.Errors.Any(x => x.Code == "DefaultError" && x.Description == "An unknown failure has occurred.")); } [Test] @@ -158,7 +208,32 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security } [Test] - public async Task GivenIDeleteAMemberRole_AndItDoesntExist_ThenTheMemberGroupShouldBeDeleted_AndIShouldGetASuccessResultAsync() + public async Task GivenIDeleteAMemberRole_AndTheIdCannotBeParsedToAnInt_ThenTheMemberGroupShouldNotBeDeleted_AndIShouldGetAFailResultAsync() + { + // arrange + MembersRoleStore sut = CreateSut(); + var fakeRole = new IdentityRole() + { + Id = "7a77", + 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(x => x.Code == "DefaultError" && x.Description == "An unknown failure has occurred.")); + } + + + [Test] + public async Task GivenIDeleteAMemberRole_AndItDoesntExist_ThenTheMemberGroupShouldNotBeDeleted_AndIShouldGetAFailResultAsync() { // arrange MembersRoleStore sut = CreateSut(); @@ -178,9 +253,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security // assert Assert.IsTrue(identityResult.Succeeded == false); - Assert.IsTrue(identityResult.Errors.Any()); + Assert.IsTrue(identityResult.Errors.Any(x=>x.Code == "InvalidRoleName" && x.Description == "Role name 'testname' is invalid.")); _mockMemberGroupService.Verify(x => x.GetById(777)); - _mockMemberGroupService.Verify(x => x.Delete(mockMemberGroup)); } [Test] @@ -188,21 +262,17 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security { // arrange MembersRoleStore sut = CreateSut(); - var fakeRole = new IdentityRole() + var fakeRole = new IdentityRole("fakeGroupName") { - Id = "777", - Name = "testname" + Id = "777" }; IEnumerable> expected = new List>() { - new IdentityRole("fakeGroupName") - { - Id = "77" - } + fakeRole }; IMemberGroup mockMemberGroup = Mock.Of(m => - m.Name == "fakeGroupName" && m.CreatorId == 77); + m.Name == "fakeGroupName" && m.CreatorId == 123 && m.Id == 777); IEnumerable fakeMemberGroups = new List() { @@ -215,7 +285,12 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security IQueryable> actual = sut.Roles; // assert - Assert.AreEqual(expected.AsQueryable(), actual); + Assert.AreEqual(expected.AsQueryable().First().Id, actual.First().Id); + Assert.AreEqual(expected.AsQueryable().First().Name, actual.First().Name); + //Always null: + //Assert.AreEqual(expected.AsQueryable().First().NormalizedName, actual.First().NormalizedName); + //Always different: + //Assert.AreEqual(expected.AsQueryable().First().ConcurrencyStamp, actual.First().ConcurrencyStamp); _mockMemberGroupService.Verify(x => x.GetAll()); } } From e124ee336aef7c2146ec528286e03df63df630b2 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Thu, 4 Mar 2021 17:29:11 +0000 Subject: [PATCH 03/78] Deleted MembersRoleProvider. Switched to role store. Made everything single not plural. Fixed formatting. --- .../Mapping/MemberTabsAndPropertiesMapper.cs | 1 + .../Security/IMemberManager.cs | 2 +- .../Security/IdentityMapDefinition.cs | 6 +- ...ityBuilder.cs => MemberIdentityBuilder.cs} | 10 +- ...ityOptions.cs => MemberIdentityOptions.cs} | 2 +- ...sIdentityUser.cs => MemberIdentityUser.cs} | 12 +- ...MembersRoleStore.cs => MemberRoleStore.cs} | 21 ++-- ...MembersUserStore.cs => MemberUserStore.cs} | 106 +++++++++--------- ...MembersServiceCollectionExtensionsTests.cs | 4 +- .../MemberIdentityUserManagerTests.cs | 42 +++---- .../Security/MemberIdentityUserStoreTests.cs | 10 +- .../Security/MemberRoleStoreTests.cs | 28 ++--- .../Controllers/MemberControllerUnitTests.cs | 40 +++---- .../Controllers/ContentController.cs | 1 + .../Controllers/MemberController.cs | 10 +- .../Controllers/MemberGroupController.cs | 41 ++++--- .../UmbracoBuilderExtensions.cs | 2 +- .../Trees/MemberGroupTreeController.cs | 2 + .../ServiceCollectionExtensions.cs | 12 +- .../Extensions/IdentityBuilderExtensions.cs | 4 +- .../Security/MemberManager.cs | 14 +-- .../Security/Providers/MembersRoleProvider.cs | 106 ------------------ src/Umbraco.Web/Umbraco.Web.csproj | 3 +- 23 files changed, 187 insertions(+), 292 deletions(-) rename src/Umbraco.Infrastructure/Security/{MembersIdentityBuilder.cs => MemberIdentityBuilder.cs} (71%) rename src/Umbraco.Infrastructure/Security/{MembersIdentityOptions.cs => MemberIdentityOptions.cs} (78%) rename src/Umbraco.Infrastructure/Security/{MembersIdentityUser.cs => MemberIdentityUser.cs} (90%) rename src/Umbraco.Infrastructure/Security/{MembersRoleStore.cs => MemberRoleStore.cs} (87%) rename src/Umbraco.Infrastructure/Security/{MembersUserStore.cs => MemberUserStore.cs} (81%) delete mode 100644 src/Umbraco.Web/Security/Providers/MembersRoleProvider.cs diff --git a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs index 92aab36bd4..9ac1b05d0f 100644 --- a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs @@ -237,6 +237,7 @@ namespace Umbraco.Cms.Core.Models.Mapping var userRoles = username.IsNullOrWhiteSpace() ? null : _memberService.GetAllRoles(username); // create a dictionary of all roles (except internal roles) + "false" + //TODO: use MembersRoleStore var result = _memberGroupService.GetAll() .Select(x => x.Name) // if a role starts with __umbracoRole we won't show it as it's an internal role used for public access 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 1a76dec2d5..50c4d1e505 100644 --- a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs +++ b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs @@ -42,10 +42,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; }, @@ -94,7 +94,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/MembersRoleStore.cs b/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs similarity index 87% rename from src/Umbraco.Infrastructure/Security/MembersRoleStore.cs rename to src/Umbraco.Infrastructure/Security/MemberRoleStore.cs index 3aafa702cd..399da9aec0 100644 --- a/src/Umbraco.Infrastructure/Security/MembersRoleStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using System.Security.Claims; @@ -7,7 +6,6 @@ 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 @@ -15,19 +13,12 @@ namespace Umbraco.Cms.Core.Security /// /// A custom user store that uses Umbraco member data /// - public class MembersRoleStore : RoleStoreBase, string, IdentityUserRole, IdentityRoleClaim> + public class MemberRoleStore : 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 MemberRoleStore(IMemberGroupService memberGroupService, IdentityErrorDescriber describer) + : base(describer) => _memberGroupService = memberGroupService ?? throw new ArgumentNullException(nameof(memberGroupService)); /// public override IQueryable> Roles @@ -94,7 +85,7 @@ namespace Umbraco.Cms.Core.Security } else { - //TODO: throw exception when not found, or return failure? And is this the correcet message + //TODO: throw exception when not found, or return failure? And is this the correct message return Task.FromResult(IdentityResult.Failed(ErrorDescriber.InvalidRoleName(role.Name))); } @@ -123,7 +114,7 @@ namespace Umbraco.Cms.Core.Security } else { - //TODO: throw exception when not found, or return failure? And is this the correcet message + //TODO: throw exception when not found, or return failure? And is this the correct message return Task.FromResult(IdentityResult.Failed(ErrorDescriber.InvalidRoleName(role.Name))); } @@ -153,6 +144,8 @@ namespace Umbraco.Cms.Core.Security return Task.FromResult(memberGroup == null ? null : MapFromMemberGroup(memberGroup)); } + ///TODO: are we implementing these claims methods? + /// public override Task> GetClaimsAsync(IdentityRole role, 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 81% rename from src/Umbraco.Infrastructure/Security/MembersUserStore.cs rename to src/Umbraco.Infrastructure/Security/MemberUserStore.cs index 9405992ba8..17a45764ad 100644 --- a/src/Umbraco.Infrastructure/Security/MembersUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs @@ -19,20 +19,20 @@ 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, string, IdentityUserClaim, IdentityUserRole, IdentityUserLogin, IdentityUserToken, IdentityRoleClaim> { 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)); @@ -45,16 +45,16 @@ namespace Umbraco.Cms.Core.Security /// /// [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) => 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) => 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(); @@ -100,7 +100,7 @@ namespace Umbraco.Cms.Core.Security } /// - public override Task UpdateAsync(MembersIdentityUser user, CancellationToken cancellationToken = default) + public override Task UpdateAsync(MemberIdentityUser user, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -121,7 +121,7 @@ namespace Umbraco.Cms.Core.Security if (found != null) { // we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. - var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(MembersIdentityUser.Logins)); + var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(MemberIdentityUser.Logins)); if (UpdateMemberProperties(found, user)) { @@ -148,7 +148,7 @@ namespace Umbraco.Cms.Core.Security } /// - public override Task DeleteAsync(MembersIdentityUser user, CancellationToken cancellationToken = default) + public override Task DeleteAsync(MemberIdentityUser user, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -170,10 +170,10 @@ namespace Umbraco.Cms.Core.Security } /// - public override Task FindByIdAsync(string userId, CancellationToken cancellationToken = default) => FindUserAsync(userId, cancellationToken); + public override Task FindByIdAsync(string userId, CancellationToken cancellationToken = default) => FindUserAsync(userId, cancellationToken); /// - protected override Task FindUserAsync(string userId, CancellationToken cancellationToken) + protected override Task FindUserAsync(string userId, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -181,30 +181,30 @@ namespace Umbraco.Cms.Core.Security 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); @@ -213,7 +213,7 @@ namespace Umbraco.Cms.Core.Security } /// - 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); @@ -227,28 +227,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(); @@ -271,7 +271,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(); @@ -290,7 +290,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,7 +308,7 @@ namespace Umbraco.Cms.Core.Security cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - MembersIdentityUser user = await FindUserAsync(userId, cancellationToken); + MemberIdentityUser user = await FindUserAsync(userId, cancellationToken); if (user == null) { return null; @@ -356,7 +356,7 @@ namespace Umbraco.Cms.Core.Security } /// - public override Task AddToRoleAsync(MembersIdentityUser user, string role, CancellationToken cancellationToken = default) + public override Task AddToRoleAsync(MemberIdentityUser user, string role, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -387,7 +387,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 +420,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 +443,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 normalizedRoleName, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -458,7 +458,7 @@ namespace Umbraco.Cms.Core.Security /// /// Lists all users of a given role. /// - public override Task> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken = default) + public override Task> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -469,7 +469,7 @@ namespace Umbraco.Cms.Core.Security IEnumerable members = _memberService.GetMembersByMemberType(normalizedRoleName); - IList membersIdentityUsers = members.Select(x => _mapper.Map(x)).ToList(); + IList membersIdentityUsers = members.Select(x => _mapper.Map(x)).ToList(); return Task.FromResult(membersIdentityUsers); } @@ -493,7 +493,7 @@ namespace Umbraco.Cms.Core.Security /// 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 +504,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,7 +519,7 @@ namespace Umbraco.Cms.Core.Security : user.SecurityStamp); } - private MembersIdentityUser AssignLoginsCallback(MembersIdentityUser user) + private MemberIdentityUser AssignLoginsCallback(MemberIdentityUser user) { if (user != null) { @@ -530,12 +530,12 @@ namespace Umbraco.Cms.Core.Security 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 +546,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 +554,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 +562,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; @@ -595,14 +595,14 @@ namespace Umbraco.Cms.Core.Security } } - 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; @@ -617,7 +617,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))) { } @@ -645,42 +645,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.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 b88189a08b..a319d33b34 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( @@ -71,12 +71,12 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security new BackOfficeIdentityErrorDescriber(), _mockServiceProviders.Object, new Mock().Object, - new Mock>>().Object, + new Mock>>().Object, _mockPasswordConfiguration.Object); validator.Setup(v => v.ValidateAsync( userManager, - It.IsAny())) + It.IsAny())) .Returns(Task.FromResult(IdentityResult.Success)).Verifiable(); return userManager; @@ -87,7 +87,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security { //arrange MemberManager sut = CreateSut(); - MembersIdentityUser fakeUser = new MembersIdentityUser() + MemberIdentityUser fakeUser = new MemberIdentityUser() { PasswordConfig = "testConfig" }; @@ -148,7 +148,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 index 8afe7fe198..8a0bf149b7 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs @@ -20,10 +20,10 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security { private Mock _mockMemberService; - public MembersUserStore CreateSut() + public MemberUserStore CreateSut() { _mockMemberService = new Mock(); - return new MembersUserStore( + return new MemberUserStore( _mockMemberService.Object, new UmbracoMapper(new MapDefinitionCollection(new List())), new Mock().Object, @@ -34,7 +34,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public void GivenICreateUser_AndTheUserIsNull_ThenIShouldGetAFailedResultAsync() { // arrange - MembersUserStore sut = CreateSut(); + MemberUserStore sut = CreateSut(); CancellationToken fakeCancellationToken = new CancellationToken(){}; // act @@ -49,8 +49,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenICreateANewUser_AndTheUserIsPopulatedCorrectly_ThenIShouldGetASuccessResultAsync() { // arrange - MembersUserStore sut = CreateSut(); - var fakeUser = new MembersIdentityUser() { }; + MemberUserStore sut = CreateSut(); + var fakeUser = new MemberIdentityUser() { }; var fakeCancellationToken = new CancellationToken() { }; IMemberType fakeMemberType = new MemberType(new MockShortStringHelper(), 77); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs index 6c3c741376..f74b6cef94 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs @@ -16,18 +16,14 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security [TestFixture] public class MemberRoleStoreTests { - private Mock _mockMemberService; private Mock _mockMemberGroupService; private IdentityErrorDescriber ErrorDescriber => new IdentityErrorDescriber(); - public MembersRoleStore CreateSut() + public MemberRoleStore CreateSut() { - _mockMemberService = new Mock(); _mockMemberGroupService = new Mock(); - return new MembersRoleStore( - _mockMemberService.Object, + return new MemberRoleStore( _mockMemberGroupService.Object, - new Mock().Object, ErrorDescriber); } @@ -35,7 +31,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public void GivenICreateAMemberRole_AndTheGroupIsNull_ThenIShouldGetAFailedResultAsync() { // arrange - MembersRoleStore sut = CreateSut(); + MemberRoleStore sut = CreateSut(); CancellationToken fakeCancellationToken = new CancellationToken() { }; // act @@ -50,7 +46,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenICreateAMemberRole_AndTheGroupIsPopulatedCorrectly_ThenIShouldGetASuccessResultAsync() { // arrange - MembersRoleStore sut = CreateSut(); + MemberRoleStore sut = CreateSut(); var fakeRole = new IdentityRole() { Id = "777", @@ -78,7 +74,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenIUpdateAMemberRole_AndTheGroupExistsWithTheSameName_ThenIShouldGetASuccessResultAsyncButNoUpdatesMade() { // arrange - MembersRoleStore sut = CreateSut(); + MemberRoleStore sut = CreateSut(); var fakeRole = new IdentityRole() { Id = "777", @@ -107,7 +103,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenIUpdateAMemberRole_AndTheGroupExistsWithADifferentSameName_ThenIShouldGetASuccessResultAsyncWithUpdatesMade() { // arrange - MembersRoleStore sut = CreateSut(); + MemberRoleStore sut = CreateSut(); var fakeRole = new IdentityRole() { Id = "777", @@ -137,7 +133,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenIUpdateAMemberRole_AndTheGroupDoesntExist_ThenIShouldGetAFailureResultAsync() { // arrange - MembersRoleStore sut = CreateSut(); + MemberRoleStore sut = CreateSut(); var fakeRole = new IdentityRole() { Id = "777", @@ -161,7 +157,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenIUpdateAMemberRole_AndTheIdCannotBeParsedToAnInt_ThenIShouldGetAFailureResultAsync() { // arrange - MembersRoleStore sut = CreateSut(); + MemberRoleStore sut = CreateSut(); var fakeRole = new IdentityRole() { Id = "7a77", @@ -184,7 +180,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenIDeleteAMemberRole_AndItExists_ThenTheMemberGroupShouldBeDeleted_AndIShouldGetASuccessResultAsync() { // arrange - MembersRoleStore sut = CreateSut(); + MemberRoleStore sut = CreateSut(); var fakeRole = new IdentityRole() { Id = "777", @@ -211,7 +207,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenIDeleteAMemberRole_AndTheIdCannotBeParsedToAnInt_ThenTheMemberGroupShouldNotBeDeleted_AndIShouldGetAFailResultAsync() { // arrange - MembersRoleStore sut = CreateSut(); + MemberRoleStore sut = CreateSut(); var fakeRole = new IdentityRole() { Id = "7a77", @@ -236,7 +232,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenIDeleteAMemberRole_AndItDoesntExist_ThenTheMemberGroupShouldNotBeDeleted_AndIShouldGetAFailResultAsync() { // arrange - MembersRoleStore sut = CreateSut(); + MemberRoleStore sut = CreateSut(); var fakeRole = new IdentityRole() { Id = "777", @@ -261,7 +257,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenIGetAllMemberRoles_ThenIShouldGetAllMemberGroups_AndASuccessResultAsync() { // arrange - MembersRoleStore sut = CreateSut(); + MemberRoleStore sut = CreateSut(); var fakeRole = new IdentityRole("fakeGroupName") { Id = "777" 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 5bb4613912..52722f75d1 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs @@ -68,7 +68,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers IMemberGroupService memberGroupService, IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - IPasswordChanger passwordChanger, + IPasswordChanger passwordChanger, IOptions globalSettings, IUser user) { @@ -78,7 +78,7 @@ 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())) @@ -107,14 +107,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())) @@ -148,14 +148,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())) @@ -190,13 +190,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 string password = "fakepassword9aw89rnyco3938cyr^%&*()i8Y"; 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); @@ -242,13 +242,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); @@ -257,7 +257,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); @@ -289,7 +289,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() { @@ -322,14 +322,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); @@ -364,7 +364,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IBackOfficeSecurity backOfficeSecurity, - IPasswordChanger passwordChanger, + IPasswordChanger passwordChanger, IOptions globalSettings, IUser user) { @@ -376,7 +376,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); @@ -384,7 +384,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.UpdateAsync(It.IsAny())) + .Setup(x => x.UpdateAsync(It.IsAny())) .ReturnsAsync(() => IdentityResult.Success); Mock.Get(memberTypeService).Setup(x => x.GetDefault()).Returns("fakeAlias"); Mock.Get(backOfficeSecurityAccessor).Setup(x => x.BackOfficeSecurity).Returns(backOfficeSecurity); @@ -429,10 +429,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) { diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index b314cb4fa1..00f2fff56c 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -2435,6 +2435,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers .Select(_umbracoMapper.Map) .ToArray(); + //TODO: change to role store 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 3c5adb8ebe..b5f8674f9e 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -57,7 +57,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; /// /// Initializes a new instance of the class. @@ -90,7 +90,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IJsonSerializer jsonSerializer, - IPasswordChanger passwordChanger) + IPasswordChanger passwordChanger) : base(cultureDictionary, loggerFactory, shortStringHelper, eventMessages, localizedTextService, jsonSerializer) { _propertyEditors = propertyEditors; @@ -355,7 +355,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, @@ -445,7 +445,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers ModelState.AddModelError("custom", "An admin cannot lock a user"); } - MembersIdentityUser identityMember = await _memberManager.FindByIdAsync(contentItem.Id.ToString()); + MemberIdentityUser identityMember = await _memberManager.FindByIdAsync(contentItem.Id.ToString()); if (identityMember == null) { return new ValidationErrorResult("Identity member was not found"); @@ -586,7 +586,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// The member content item /// The member as an identity user - private async Task AddOrUpdateRoles(MemberSave contentItem, MembersIdentityUser identityMember) + private async Task AddOrUpdateRoles(MemberSave contentItem, MemberIdentityUser identityMember) { // We're gonna look up the current roles now because the below code can cause // events to be raised and developers could be manually adding roles to members in diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs index 7146cd5820..3ea1d615b8 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Mapping; @@ -26,17 +28,19 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private readonly IMemberGroupService _memberGroupService; private readonly UmbracoMapper _umbracoMapper; private readonly ILocalizedTextService _localizedTextService; - + private readonly RoleManager> _roleManager; + public MemberGroupController( IMemberGroupService memberGroupService, UmbracoMapper umbracoMapper, - ILocalizedTextService localizedTextService + ILocalizedTextService localizedTextService, + RoleManager> roleManager ) { _memberGroupService = memberGroupService ?? throw new ArgumentNullException(nameof(memberGroupService)); + _roleManager = roleManager ?? throw new ArgumentNullException(nameof(roleManager)); _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); - _localizedTextService = - localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); + _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); } /// @@ -46,13 +50,13 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// public ActionResult GetById(int id) { - var memberGroup = _memberGroupService.GetById(id); + IdentityRole memberGroup = _roleManager.FindByIdAsync(id.ToString()).Result; if (memberGroup == null) { return NotFound(); } - var dto = _umbracoMapper.Map(memberGroup); + MemberGroupDisplay dto = _umbracoMapper.Map, MemberGroupDisplay>(memberGroup); return dto; } @@ -64,7 +68,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 +86,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(); @@ -95,15 +101,22 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers public IEnumerable GetByIds([FromQuery]int[] ids) { - return _memberGroupService.GetByIds(ids) - .Select(_umbracoMapper.Map); + var roles = new List>(); + + foreach (int id in ids) + { + Task> role = _roleManager.FindByIdAsync(id.ToString()); + roles.Add(role.Result); + } + + return roles.Select(x=> _umbracoMapper.Map, MemberGroupDisplay>(x)); } [HttpDelete] [HttpPost] public IActionResult DeleteById(int id) { - var memberGroup = _memberGroupService.GetById(id); + IMemberGroup memberGroup = _memberGroupService.GetById(id); if (memberGroup == null) { return NotFound(); @@ -113,11 +126,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return Ok(); } - public IEnumerable GetAllGroups() - { - return _memberGroupService.GetAll() - .Select(_umbracoMapper.Map); - } + public IEnumerable GetAllGroups() => _roleManager.Roles.Select(x => _umbracoMapper.Map, MemberGroupDisplay>(x)); public MemberGroupDisplay GetEmpty() { diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index 53ea801490..6be3008a32 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -86,7 +86,7 @@ namespace Umbraco.Extensions builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique, PasswordChanger>(); - builder.Services.AddUnique, PasswordChanger>(); + builder.Services.AddUnique, PasswordChanger>(); return builder; } diff --git a/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs index 7445f35f29..5888068656 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs @@ -32,6 +32,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees _memberGroupService = memberGroupService; } + //TODO: change to role store protected override IEnumerable GetTreeNodesFromService(string id, FormCollection queryStrings) { return _memberGroupService.GetAll() @@ -49,6 +50,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees var root = rootResult.Value; //check if there are any groups + //TODO: change to role store root.HasChildren = _memberGroupService.GetAll().Any(); return root; } diff --git a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs index 8b81fc673c..39a6b2906d 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs @@ -65,17 +65,17 @@ namespace Umbraco.Extensions public static void AddMembersIdentity(this IServiceCollection services) => services.BuildMembersIdentity() .AddDefaultTokenProviders() - .AddUserStore() + .AddUserStore() .AddMembersManager(); - 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(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..b1c2944565 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 current instance. public static IdentityBuilder AddMembersManager(this IdentityBuilder identityBuilder) - where TUserManager : UserManager, TInterface + 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 c36ae0c0fc..ae91852f46 100644 --- a/src/Umbraco.Web.Common/Security/MemberManager.cs +++ b/src/Umbraco.Web.Common/Security/MemberManager.cs @@ -16,21 +16,21 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Web.Common.Security { - public class MemberManager : UmbracoUserManager, IMemberManager + public class MemberManager : UmbracoUserManager, IMemberManager { private readonly IHttpContextAccessor _httpContextAccessor; 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, IHttpContextAccessor httpContextAccessor, - 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 93cb8da83d..fbe21702f9 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -176,7 +176,6 @@ - @@ -229,4 +228,4 @@ - + \ No newline at end of file From 3d53690048ff8b45c614b6057a4627606c60de98 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Sat, 6 Mar 2021 10:53:34 +0000 Subject: [PATCH 04/78] Add role store to service collection. Updated tests with new role methods. --- .../Services/IMemberGroupService.cs | 2 +- .../Security/MemberRoleStore.cs | 147 +++++++++----- .../Services/Implement/MemberGroupService.cs | 21 +- .../Security/MemberRoleStoreTests.cs | 185 +++++++++++++----- .../Umbraco.Tests.UnitTests.csproj | 6 + .../Controllers/MemberGroupController.cs | 6 +- .../ServiceCollectionExtensions.cs | 18 +- .../Extensions/IdentityBuilderExtensions.cs | 6 +- .../Umbraco.Web.Common.csproj | 1 + 9 files changed, 290 insertions(+), 102 deletions(-) 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/Security/MemberRoleStore.cs b/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs index 399da9aec0..0b91237af9 100644 --- a/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; @@ -13,35 +10,27 @@ namespace Umbraco.Cms.Core.Security /// /// A custom user store that uses Umbraco member data /// - public class MemberRoleStore : RoleStoreBase, string, IdentityUserRole, IdentityRoleClaim> + public class MemberRoleStore : IRoleStore where TRole : IdentityRole { private readonly IMemberGroupService _memberGroupService; + private bool _disposed; - public MemberRoleStore(IMemberGroupService memberGroupService, IdentityErrorDescriber describer) - : base(describer) => _memberGroupService = memberGroupService ?? throw new ArgumentNullException(nameof(memberGroupService)); - - /// - public override IQueryable> Roles + public MemberRoleStore(IMemberGroupService memberGroupService, IdentityErrorDescriber errorDescriber) { - get - { - IEnumerable memberGroups = _memberGroupService.GetAll(); - var identityRoles = new List>(); - foreach (IMemberGroup group in memberGroups) - { - IdentityRole identityRole = MapFromMemberGroup(group); - identityRoles.Add(identityRole); - } - - return identityRoles.AsQueryable(); - } + _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 override Task CreateAsync( - IdentityRole role, - CancellationToken cancellationToken = new CancellationToken()) + public Task CreateAsync(TRole role, CancellationToken cancellationToken = new CancellationToken()) { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); if (role == null) { throw new ArgumentNullException(nameof(role)); @@ -61,9 +50,10 @@ namespace Umbraco.Cms.Core.Security /// - public override Task UpdateAsync(IdentityRole role, - CancellationToken cancellationToken = new CancellationToken()) + public Task UpdateAsync(TRole role, CancellationToken cancellationToken = new CancellationToken()) { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); if (role == null) { throw new ArgumentNullException(nameof(role)); @@ -93,9 +83,10 @@ namespace Umbraco.Cms.Core.Security } /// - public override Task DeleteAsync(IdentityRole role, - CancellationToken cancellationToken = new CancellationToken()) + public Task DeleteAsync(TRole role, CancellationToken cancellationToken = new CancellationToken()) { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); if (role == null) { throw new ArgumentNullException(nameof(role)); @@ -122,9 +113,60 @@ namespace Umbraco.Cms.Core.Security } /// - public override Task> FindByIdAsync(string id, - CancellationToken cancellationToken = new CancellationToken()) + + public Task GetRoleIdAsync(TRole role, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + + return Task.FromResult(role.Id); + } + + public Task GetRoleNameAsync(TRole role, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (!int.TryParse(role.Id, out int roleId)) + { + return null; + } + + IMemberGroup memberGroup = _memberGroupService.GetById(roleId); + + return Task.FromResult(memberGroup?.Name); + } + + public Task SetRoleNameAsync(TRole role, string roleName, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + return null; + } + + public Task GetNormalizedRoleNameAsync(TRole role, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + return null; + } + + public Task SetNormalizedRoleNameAsync(TRole role, string normalizedName, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + return null; + } + + /// + public Task FindByIdAsync(string id, CancellationToken cancellationToken = new CancellationToken()) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); if (!int.TryParse(id, out int roleId)) { return null; @@ -136,39 +178,34 @@ namespace Umbraco.Cms.Core.Security } /// - public override Task> FindByNameAsync(string normalizedName, - CancellationToken cancellationToken = new CancellationToken()) + public Task FindByNameAsync(string name, CancellationToken cancellationToken = new CancellationToken()) { - IMemberGroup memberGroup = _memberGroupService.GetByName(normalizedName); + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + IMemberGroup memberGroup = _memberGroupService.GetByName(name); return Task.FromResult(memberGroup == null ? null : MapFromMemberGroup(memberGroup)); } - ///TODO: are we implementing these claims methods? - - /// - 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) + private TRole MapFromMemberGroup(IMemberGroup memberGroup) { var result = new IdentityRole { Id = memberGroup.Id.ToString(), Name = memberGroup.Name + //TODO: Are we interested in NormalizedRoleName? }; - return result; + return result as TRole; } /// @@ -177,7 +214,7 @@ namespace Umbraco.Cms.Core.Security /// /// /// - private bool MapToMemberGroup(IdentityRole role, IMemberGroup memberGroup) + private bool MapToMemberGroup(TRole role, IMemberGroup memberGroup) { var anythingChanged = false; @@ -189,5 +226,21 @@ namespace Umbraco.Cms.Core.Security return anythingChanged; } + + public void Dispose() + { + //TODO: is any dispose action necessary here or is this all managed by the IOC container? + } + + /// + /// Throws if this class has been disposed. + /// + protected void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name); + } + } } } diff --git a/src/Umbraco.Infrastructure/Services/Implement/MemberGroupService.cs b/src/Umbraco.Infrastructure/Services/Implement/MemberGroupService.cs index 5e6138980a..1f6d4743be 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; @@ -130,6 +130,25 @@ namespace Umbraco.Cms.Core.Services.Implement } } + + public void GetByRole(IMemberGroup memberGroup) + { + using (var scope = ScopeProvider.CreateScope()) + { + var deleteEventArgs = new DeleteEventArgs(memberGroup); + if (scope.Events.DispatchCancelable(Deleting, this, deleteEventArgs)) + { + scope.Complete(); + return; + } + + _memberGroupRepository.Delete(memberGroup); + scope.Complete(); + deleteEventArgs.CanCancel = false; + scope.Events.Dispatch(Deleted, this, deleteEventArgs); + } + } + /// /// Occurs before Delete of a member group /// diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs index f74b6cef94..ed77c63afc 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -7,7 +6,6 @@ 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; @@ -19,19 +17,19 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security private Mock _mockMemberGroupService; private IdentityErrorDescriber ErrorDescriber => new IdentityErrorDescriber(); - public MemberRoleStore CreateSut() + public MemberRoleStore CreateSut() { _mockMemberGroupService = new Mock(); - return new MemberRoleStore( + return new MemberRoleStore( _mockMemberGroupService.Object, ErrorDescriber); } [Test] - public void GivenICreateAMemberRole_AndTheGroupIsNull_ThenIShouldGetAFailedResultAsync() + public void GivenICreateAMemberRole_AndTheGroupIsNull_ThenIShouldGetAnArgumentException() { // arrange - MemberRoleStore sut = CreateSut(); + MemberRoleStore sut = CreateSut(); CancellationToken fakeCancellationToken = new CancellationToken() { }; // act @@ -46,8 +44,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenICreateAMemberRole_AndTheGroupIsPopulatedCorrectly_ThenIShouldGetASuccessResultAsync() { // arrange - MemberRoleStore sut = CreateSut(); - var fakeRole = new IdentityRole() + MemberRoleStore sut = CreateSut(); + var fakeRole = new IdentityRole { Id = "777", Name = "testname" @@ -74,8 +72,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenIUpdateAMemberRole_AndTheGroupExistsWithTheSameName_ThenIShouldGetASuccessResultAsyncButNoUpdatesMade() { // arrange - MemberRoleStore sut = CreateSut(); - var fakeRole = new IdentityRole() + MemberRoleStore sut = CreateSut(); + var fakeRole = new IdentityRole { Id = "777", Name = "fakeGroupName" @@ -103,8 +101,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenIUpdateAMemberRole_AndTheGroupExistsWithADifferentSameName_ThenIShouldGetASuccessResultAsyncWithUpdatesMade() { // arrange - MemberRoleStore sut = CreateSut(); - var fakeRole = new IdentityRole() + MemberRoleStore sut = CreateSut(); + var fakeRole = new IdentityRole { Id = "777", Name = "fakeGroup777" @@ -133,8 +131,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenIUpdateAMemberRole_AndTheGroupDoesntExist_ThenIShouldGetAFailureResultAsync() { // arrange - MemberRoleStore sut = CreateSut(); - var fakeRole = new IdentityRole() + MemberRoleStore sut = CreateSut(); + var fakeRole = new IdentityRole { Id = "777", Name = "testname" @@ -157,31 +155,29 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenIUpdateAMemberRole_AndTheIdCannotBeParsedToAnInt_ThenIShouldGetAFailureResultAsync() { // arrange - MemberRoleStore sut = CreateSut(); - var fakeRole = new IdentityRole() + MemberRoleStore sut = CreateSut(); + var fakeRole = new IdentityRole { Id = "7a77", Name = "testname" }; var fakeCancellationToken = new CancellationToken() { }; - - bool raiseEvents = false; - - + // act IdentityResult identityResult = await sut.UpdateAsync(fakeRole, fakeCancellationToken); // assert Assert.IsTrue(identityResult.Succeeded == false); Assert.IsTrue(identityResult.Errors.Any(x => x.Code == "DefaultError" && x.Description == "An unknown failure has occurred.")); + _mockMemberGroupService.VerifyNoOtherCalls(); } [Test] public async Task GivenIDeleteAMemberRole_AndItExists_ThenTheMemberGroupShouldBeDeleted_AndIShouldGetASuccessResultAsync() { // arrange - MemberRoleStore sut = CreateSut(); - var fakeRole = new IdentityRole() + MemberRoleStore sut = CreateSut(); + var fakeRole = new IdentityRole { Id = "777", Name = "testname" @@ -201,14 +197,15 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security 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_AndIShouldGetAFailResultAsync() { // arrange - MemberRoleStore sut = CreateSut(); - var fakeRole = new IdentityRole() + MemberRoleStore sut = CreateSut(); + var fakeRole = new IdentityRole { Id = "7a77", Name = "testname" @@ -225,6 +222,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security // assert Assert.IsTrue(identityResult.Succeeded == false); Assert.IsTrue(identityResult.Errors.Any(x => x.Code == "DefaultError" && x.Description == "An unknown failure has occurred.")); + _mockMemberGroupService.VerifyNoOtherCalls(); } @@ -232,8 +230,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenIDeleteAMemberRole_AndItDoesntExist_ThenTheMemberGroupShouldNotBeDeleted_AndIShouldGetAFailResultAsync() { // arrange - MemberRoleStore sut = CreateSut(); - var fakeRole = new IdentityRole() + MemberRoleStore sut = CreateSut(); + var fakeRole = new IdentityRole { Id = "777", Name = "testname" @@ -251,43 +249,142 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security Assert.IsTrue(identityResult.Succeeded == false); Assert.IsTrue(identityResult.Errors.Any(x=>x.Code == "InvalidRoleName" && x.Description == "Role name 'testname' is invalid.")); _mockMemberGroupService.Verify(x => x.GetById(777)); + _mockMemberGroupService.VerifyNoOtherCalls(); } [Test] - public async Task GivenIGetAllMemberRoles_ThenIShouldGetAllMemberGroups_AndASuccessResultAsync() + public async Task GivenIFindAMemberRoleByRoleId_AndRoleIdExists_ThenIShouldGetASuccessResultAsync() { // arrange - MemberRoleStore sut = CreateSut(); + MemberRoleStore sut = CreateSut(); var fakeRole = new IdentityRole("fakeGroupName") { Id = "777" }; - IEnumerable> expected = new List>() + int fakeRoleId = 777; + + IMemberGroup mockMemberGroup = Mock.Of(m => + m.Name == "fakeGroupName" && + m.CreatorId == 123 && + m.Id == 777); + + var fakeCancellationToken = new CancellationToken() { }; + + _mockMemberGroupService.Setup(x => x.GetById(fakeRoleId)).Returns(mockMemberGroup); + + // act + IdentityRole actual = await sut.FindByIdAsync(fakeRole.Id, fakeCancellationToken); + + // assert + Assert.AreEqual(fakeRole.Name, actual.Name); + Assert.AreEqual(fakeRole.Id, actual.Id); + _mockMemberGroupService.Verify(x => x.GetById(fakeRoleId)); + _mockMemberGroupService.VerifyNoOtherCalls(); + } + + [Test] + public void GivenIFindAMemberRoleByRoleId_AndIdCannotBeParsedToAnInt_ThenIShouldGetAFailureResultAsync() + { + // arrange + MemberRoleStore sut = CreateSut(); + var fakeRole = new IdentityRole { - fakeRole + Id = "7a77", + Name = "testname" + }; + var fakeCancellationToken = new CancellationToken() { }; + + // act + Task actual = sut.FindByIdAsync(fakeRole.Id, fakeCancellationToken); + + // assert + Assert.IsNull(actual); + _mockMemberGroupService.VerifyNoOtherCalls(); + } + + [Test] + public async Task GivenIFindAMemberRoleByRoleName_AndRoleNameExists_ThenIShouldGetASuccessResultAsync() + { + // arrange + MemberRoleStore sut = CreateSut(); + var fakeRole = new IdentityRole("fakeGroupName") + { + Id = "777" }; IMemberGroup mockMemberGroup = Mock.Of(m => - m.Name == "fakeGroupName" && m.CreatorId == 123 && m.Id == 777); + m.Name == "fakeGroupName" && + m.CreatorId == 123 && + m.Id == 777); - IEnumerable fakeMemberGroups = new List() - { - mockMemberGroup - }; - _mockMemberGroupService.Setup(x => x.GetAll()).Returns(fakeMemberGroups); + _mockMemberGroupService.Setup(x => x.GetByName(fakeRole.Name)).Returns(mockMemberGroup); // act - IQueryable> actual = sut.Roles; + IdentityRole actual = await sut.FindByNameAsync(fakeRole.Name); // assert - Assert.AreEqual(expected.AsQueryable().First().Id, actual.First().Id); - Assert.AreEqual(expected.AsQueryable().First().Name, actual.First().Name); - //Always null: - //Assert.AreEqual(expected.AsQueryable().First().NormalizedName, actual.First().NormalizedName); - //Always different: - //Assert.AreEqual(expected.AsQueryable().First().ConcurrencyStamp, actual.First().ConcurrencyStamp); - _mockMemberGroupService.Verify(x => x.GetAll()); + 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(); + var fakeRole = new IdentityRole + { + Id = "777" + }; + 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(); + var fakeRole = new IdentityRole("fakeGroupName") + { + Id = "777" + }; + string fakeRoleId = fakeRole.Id; + + var fakeCancellationToken = new CancellationToken(); + + // act + Task actual = sut.GetRoleIdAsync(fakeRole, fakeCancellationToken); + + // assert + Assert.That(actual, Throws.ArgumentNullException); + + + // assert + Assert.AreEqual(fakeRoleId, actual.Result); } } } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj index bcb4022ec7..a85b158810 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj @@ -38,4 +38,10 @@ + + + ..\Umbraco.Web\bin\Debug\Umbraco.Infrastructure.dll + + + diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs index 3ea1d615b8..df76736de9 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs @@ -28,13 +28,13 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private readonly IMemberGroupService _memberGroupService; private readonly UmbracoMapper _umbracoMapper; private readonly ILocalizedTextService _localizedTextService; - private readonly RoleManager> _roleManager; + private readonly RoleManager _roleManager; public MemberGroupController( IMemberGroupService memberGroupService, UmbracoMapper umbracoMapper, ILocalizedTextService localizedTextService, - RoleManager> roleManager + RoleManager roleManager ) { _memberGroupService = memberGroupService ?? throw new ArgumentNullException(nameof(memberGroupService)); @@ -105,7 +105,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers foreach (int id in ids) { - Task> role = _roleManager.FindByIdAsync(id.ToString()); + Task role = _roleManager.FindByIdAsync(id.ToString()); roles.Add(role.Result); } diff --git a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs index 39a6b2906d..b123a659c5 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; @@ -62,12 +63,23 @@ namespace Umbraco.Extensions /// /// Adds the services required for using Members Identity /// - public static void AddMembersIdentity(this IServiceCollection services) => + public static void AddMembersIdentity(this IServiceCollection services) + { services.BuildMembersIdentity() .AddDefaultTokenProviders() + .AddMemberManager() + //.AddRoles() .AddUserStore() - .AddMembersManager(); + .AddRoleStore>() + .AddRoleValidator>() + .AddRoleManager>(); + //services.AddScoped>( + // s => new UserClaimsPrincipalFactory( + // s.GetService(), + // s.GetService>(), + // s.GetService>())); + } private static MemberIdentityBuilder BuildMembersIdentity(this IServiceCollection services) { @@ -75,7 +87,7 @@ namespace Umbraco.Extensions services.TryAddScoped, UserValidator>(); services.TryAddScoped, PasswordValidator>(); services.TryAddScoped, PasswordHasher>(); - return new MemberIdentityBuilder(services); + 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 b1c2944565..6940221518 100644 --- a/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs @@ -12,10 +12,10 @@ namespace Umbraco.Extensions /// /// 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) + public static IdentityBuilder AddMemberManager(this IdentityBuilder identityBuilder) where TUserManager : UserManager, TInterface { identityBuilder.Services.AddScoped(typeof(TInterface), typeof(TUserManager)); diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index 2bcf4a3e55..3dc8575dd7 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -22,6 +22,7 @@ + From 05ccb9e8bbe6285521653c6e4652f5b9cd65c355 Mon Sep 17 00:00:00 2001 From: emmagarland Date: Sat, 6 Mar 2021 13:44:02 +0000 Subject: [PATCH 05/78] Extended and updated unit tests. Put store methods into try and catch where accessing a service. Added more identity results and try/catches where needed. Updated memberuserstore. Ignore temporary test cached files. --- .gitignore | 1 + .../Security/MemberRoleStore.cs | 294 ++++++++++------ .../Security/MemberUserStore.cs | 320 ++++++++++++------ .../Security/MemberRoleStoreTests.cs | 30 +- 4 files changed, 411 insertions(+), 234 deletions(-) diff --git a/.gitignore b/.gitignore index 0cffac8343..f8a2ce511f 100644 --- a/.gitignore +++ b/.gitignore @@ -201,3 +201,4 @@ 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/ diff --git a/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs b/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs index 0b91237af9..4477c4870c 100644 --- a/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs @@ -15,6 +15,11 @@ namespace Umbraco.Cms.Core.Security 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" }; + public MemberRoleStore(IMemberGroupService memberGroupService, IdentityErrorDescriber errorDescriber) { _memberGroupService = memberGroupService ?? throw new ArgumentNullException(nameof(memberGroupService)); @@ -25,99 +30,123 @@ namespace Umbraco.Cms.Core.Security /// Gets or sets the for any error that occurred with the current operation. /// public IdentityErrorDescriber ErrorDescriber { get; set; } - - /// - public Task CreateAsync(TRole role, CancellationToken cancellationToken = new CancellationToken()) - { - 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(TRole role, CancellationToken cancellationToken = new CancellationToken()) + public Task CreateAsync(TRole role, CancellationToken cancellationToken = default) { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (role == null) + try { - throw new ArgumentNullException(nameof(role)); - } + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); - if (!int.TryParse(role.Id, out int roleId)) - { - //TODO: what identity error should we return in this case? - return Task.FromResult(IdentityResult.Failed(ErrorDescriber.DefaultError())); - } - - IMemberGroup memberGroup = _memberGroupService.GetById(roleId); - if (memberGroup != null) - { - if (MapToMemberGroup(role, memberGroup)) + if (role == null) { - _memberGroupService.Save(memberGroup); + throw new ArgumentNullException(nameof(role)); } - } - else - { - //TODO: throw exception when not found, or return failure? And is this the correct message - return Task.FromResult(IdentityResult.Failed(ErrorDescriber.InvalidRoleName(role.Name))); - } - return Task.FromResult(IdentityResult.Success); + var memberGroup = new MemberGroup + { + Name = role.Name + }; + + _memberGroupService.Save(memberGroup); + + role.Id = memberGroup.Id.ToString(); + + return Task.FromResult(IdentityResult.Success); + } + catch (Exception ex) + { + return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = ex.Message, Description = ex.Message })); + } + } + + + /// + public Task UpdateAsync(TRole role, CancellationToken cancellationToken = default) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + + ThrowIfDisposed(); + + 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); + } + //TODO: if nothing changed, do we need to report this? + return Task.FromResult(IdentityResult.Success); + } + else + { + //TODO: throw exception when not found, or return failure? + return Task.FromResult(IdentityResult.Failed(_memberGroupNotFoundError)); + } + + } + catch (Exception ex) + { + return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = ex.Message, Description = ex.Message })); + } } /// - public Task DeleteAsync(TRole role, CancellationToken cancellationToken = new CancellationToken()) + public Task DeleteAsync(TRole role, CancellationToken cancellationToken = default) { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (role == null) + try { - throw new ArgumentNullException(nameof(role)); - } + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); - if (!int.TryParse(role.Id, out int roleId)) - { - //TODO: what identity error should we return in this case? - return Task.FromResult(IdentityResult.Failed(ErrorDescriber.DefaultError())); - } + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } - IMemberGroup memberGroup = _memberGroupService.GetById(roleId); - if (memberGroup != null) - { - _memberGroupService.Delete(memberGroup); - } - else - { - //TODO: throw exception when not found, or return failure? And is this the correct message - return Task.FromResult(IdentityResult.Failed(ErrorDescriber.InvalidRoleName(role.Name))); - } + if (!int.TryParse(role.Id, out int roleId)) + { + //TODO: what identity error should we return in this case? + return Task.FromResult(IdentityResult.Failed(_intParseError)); + } - return Task.FromResult(IdentityResult.Success); + IMemberGroup memberGroup = _memberGroupService.GetById(roleId); + if (memberGroup != null) + { + _memberGroupService.Delete(memberGroup); + } + else + { + return Task.FromResult(IdentityResult.Failed(_memberGroupNotFoundError)); + } + + return Task.FromResult(IdentityResult.Success); + } + catch (Exception ex) + { + return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = ex.Message, Description = ex.Message })); + } } /// - public Task GetRoleIdAsync(TRole role, CancellationToken cancellationToken) + public Task GetRoleIdAsync(TRole role, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); + if (role == null) { throw new ArgumentNullException(nameof(role)); @@ -126,67 +155,118 @@ namespace Umbraco.Cms.Core.Security return Task.FromResult(role.Id); } - public Task GetRoleNameAsync(TRole role, CancellationToken cancellationToken) + /// + public Task GetRoleNameAsync(TRole role, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - if (!int.TryParse(role.Id, out int roleId)) + if (role == null) { - return null; + throw new ArgumentNullException(nameof(role)); } - IMemberGroup memberGroup = _memberGroupService.GetById(roleId); - - return Task.FromResult(memberGroup?.Name); - } - - public Task SetRoleNameAsync(TRole role, string roleName, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - return null; - } - - public Task GetNormalizedRoleNameAsync(TRole role, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - return null; - } - - public Task SetNormalizedRoleNameAsync(TRole role, string normalizedName, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - return null; + return Task.FromResult(role.Name); } /// - public Task FindByIdAsync(string id, CancellationToken cancellationToken = new CancellationToken()) + public Task SetRoleNameAsync(TRole role, string roleName, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - if (!int.TryParse(id, out int roleId)) + + if (role == null) { - return null; + throw new ArgumentNullException(nameof(role)); + } + + if (!int.TryParse(role.Id, out int roleId)) + { + //TODO: what identity error should we return in this case? + return Task.FromResult(IdentityResult.Failed(ErrorDescriber.DefaultError())); } IMemberGroup memberGroup = _memberGroupService.GetById(roleId); + if (memberGroup != null) + { + //TODO: confirm logic + memberGroup.Name = roleName; + _memberGroupService.Save(memberGroup); + role.Name = roleName; + } + else + { + return Task.FromResult(IdentityResult.Failed(_memberGroupNotFoundError)); + } + + return Task.CompletedTask; + } + + /// + public Task GetNormalizedRoleNameAsync(TRole role, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + + //TODO: are we utilising NormalizedRoleName? + return Task.FromResult(role.NormalizedName); + } + + /// + public Task SetNormalizedRoleNameAsync(TRole role, string normalizedName, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + + //TODO: are we utilising NormalizedRoleName and do we need to set it in the memberGroupService? + role.NormalizedName = normalizedName; + + return Task.CompletedTask; + } + + /// + public Task FindByIdAsync(string roleId, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (string.IsNullOrWhiteSpace(roleId)) + { + throw new ArgumentNullException(nameof(roleId)); + } + + if (!int.TryParse(roleId, out int id)) + { + throw new ArgumentOutOfRangeException(nameof(roleId), $"{nameof(roleId)} is not a valid Int"); + } + + IMemberGroup memberGroup = _memberGroupService.GetById(id); return Task.FromResult(memberGroup == null ? null : MapFromMemberGroup(memberGroup)); } /// - public Task FindByNameAsync(string name, CancellationToken cancellationToken = new CancellationToken()) + public Task FindByNameAsync(string name, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - if (name == null) + + if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentNullException(nameof(name)); } IMemberGroup memberGroup = _memberGroupService.GetByName(name); + //TODO: throw exception when not found? return Task.FromResult(memberGroup == null ? null : MapFromMemberGroup(memberGroup)); } @@ -227,10 +307,12 @@ namespace Umbraco.Cms.Core.Security return anythingChanged; } - public void Dispose() - { - //TODO: is any dispose action necessary here or is this all managed by the IOC container? - } + //TODO: is any dispose action necessary here? + + /// + /// Dispose the store + /// + public void Dispose() => _disposed = true; /// /// Throws if this class has been disposed. diff --git a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs index 17a45764ad..1d871e4134 100644 --- a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs @@ -19,7 +19,7 @@ namespace Umbraco.Cms.Core.Security /// /// A custom user store that uses Umbraco member data /// - public class MemberUserStore : UserStoreBase, string, IdentityUserClaim, IdentityUserRole, IdentityUserLogin, IdentityUserToken, IdentityRoleClaim> + public class MemberUserStore : UserStoreBase, IdentityUserRole, IdentityUserLogin, IdentityUserToken, IdentityRoleClaim> { private readonly IMemberService _memberService; private readonly UmbracoMapper _mapper; @@ -40,6 +40,7 @@ namespace Umbraco.Cms.Core.Security _scopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider)); } + //TODO: why is this not supported? /// /// Not supported in Umbraco /// @@ -56,117 +57,140 @@ namespace Umbraco.Cms.Core.Security /// public override Task CreateAsync(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)); + } + + // 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); } - - // 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) + catch (Exception ex) { - throw new DataException("Could not create the member, check logs for details"); + return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = ex.Message, Description = ex.Message })); } - - // 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(MemberIdentityUser user, CancellationToken cancellationToken = default) { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) + try { - 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(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))); - //} + throw new ArgumentNullException(nameof(user)); } - scope.Complete(); - } + 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"); + } - return Task.FromResult(IdentityResult.Success); + 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) + { + return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = ex.Message, Description = ex.Message })); + } } /// public override Task DeleteAsync(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) + 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) { - _memberService.Delete(found); + return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = ex.Message, Description = ex.Message })); } - - // TODO: when to implement external login service? - //_externalLoginService.DeleteUserLogins(UserIdToInt(user.Id)); - - return Task.FromResult(IdentityResult.Success); } /// @@ -178,6 +202,16 @@ namespace Umbraco.Cms.Core.Security cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); + if (string.IsNullOrWhiteSpace(userId)) + { + throw new ArgumentNullException(nameof(userId)); + } + + if (!Guid.TryParse(userId, out Guid id)) + { + throw new ArgumentOutOfRangeException(nameof(userId), $"{nameof(userId)} is not a valid x"); + } + IMember user = _memberService.GetById(UserIdToInt(userId)); if (user == null) { @@ -208,7 +242,8 @@ namespace Umbraco.Cms.Core.Security { 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; } @@ -216,7 +251,7 @@ namespace Umbraco.Cms.Core.Security 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 @@ -262,8 +297,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); @@ -280,6 +329,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) { @@ -311,21 +370,34 @@ namespace Umbraco.Cms.Core.Security MemberIdentityUser user = await FindUserAsync(userId, cancellationToken); if (user == null) { - return null; + //TODO: error throw or null result? + return await Task.FromResult((IdentityUserLogin)null); + } + + if (string.IsNullOrWhiteSpace(loginProvider)) + { + throw new ArgumentNullException(nameof(loginProvider)); + } + + if (string.IsNullOrWhiteSpace(providerKey)) + { + throw new ArgumentNullException(nameof(providerKey)); } IList logins = await GetLoginsAsync(user, cancellationToken); UserLoginInfo found = logins.FirstOrDefault(x => x.ProviderKey == providerKey && x.LoginProvider == loginProvider); if (found == null) { - return null; + //TODO: error throw or null result? + 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 +408,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,7 +432,8 @@ 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 }); } @@ -358,7 +441,11 @@ namespace Umbraco.Cms.Core.Security /// public override Task AddToRoleAsync(MemberIdentityUser user, string role, CancellationToken cancellationToken = default) { - cancellationToken.ThrowIfCancellationRequested(); + if (cancellationToken != null) + { + cancellationToken.ThrowIfCancellationRequested(); + } + ThrowIfDisposed(); if (user == null) { @@ -443,7 +530,7 @@ namespace Umbraco.Cms.Core.Security /// /// Returns true if a user is in the role /// - public override Task IsInRoleAsync(MemberIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default) + public override Task IsInRoleAsync(MemberIdentityUser user, string roleName, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -452,22 +539,28 @@ 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(); @@ -475,18 +568,23 @@ namespace Umbraco.Cms.Core.Security } /// - 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() }); } @@ -523,7 +621,7 @@ namespace Umbraco.Cms.Core.Security { if (user != null) { - //TODO: when to + //TODO: implement //user.SetLoginsCallback(new Lazy>(() => _externalLoginService.GetAll(UserIdToInt(user.Id)))); } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs index ed77c63afc..3b78bc1b39 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs @@ -162,7 +162,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security Name = "testname" }; var fakeCancellationToken = new CancellationToken() { }; - + // act IdentityResult identityResult = await sut.UpdateAsync(fakeRole, fakeCancellationToken); @@ -247,7 +247,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security // assert Assert.IsTrue(identityResult.Succeeded == false); - Assert.IsTrue(identityResult.Errors.Any(x=>x.Code == "InvalidRoleName" && x.Description == "Role name 'testname' is invalid.")); + Assert.IsTrue(identityResult.Errors.Any(x => x.Code == "InvalidRoleName" && x.Description == "Role name 'testname' is invalid.")); _mockMemberGroupService.Verify(x => x.GetById(777)); _mockMemberGroupService.VerifyNoOtherCalls(); } @@ -257,23 +257,23 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security { // arrange MemberRoleStore sut = CreateSut(); - var fakeRole = new IdentityRole("fakeGroupName") + var fakeRole = new IdentityRole("fakeGroupName") { Id = "777" }; int fakeRoleId = 777; - IMemberGroup mockMemberGroup = Mock.Of(m => - m.Name == "fakeGroupName" && - m.CreatorId == 123 && - m.Id == 777); + IMemberGroup fakeMemberGroup = new MemberGroup() + { + Name = "fakeGroupName", + CreatorId = 123, + Id = 777 + }; - var fakeCancellationToken = new CancellationToken() { }; - - _mockMemberGroupService.Setup(x => x.GetById(fakeRoleId)).Returns(mockMemberGroup); + _mockMemberGroupService.Setup(x => x.GetById(fakeRoleId)).Returns(fakeMemberGroup); // act - IdentityRole actual = await sut.FindByIdAsync(fakeRole.Id, fakeCancellationToken); + IdentityRole actual = await sut.FindByIdAsync(fakeRole.Id); // assert Assert.AreEqual(fakeRole.Name, actual.Name); @@ -293,7 +293,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security Name = "testname" }; var fakeCancellationToken = new CancellationToken() { }; - + // act Task actual = sut.FindByIdAsync(fakeRole.Id, fakeCancellationToken); @@ -342,7 +342,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security // act Action actual = () => sut.FindByNameAsync(fakeRole.Name, fakeCancellationToken); - + // assert Assert.That(actual, Throws.ArgumentNullException); _mockMemberGroupService.VerifyNoOtherCalls(); @@ -379,10 +379,6 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security // act Task actual = sut.GetRoleIdAsync(fakeRole, fakeCancellationToken); - // assert - Assert.That(actual, Throws.ArgumentNullException); - - // assert Assert.AreEqual(fakeRoleId, actual.Result); } From b1685905e3fb0a0a45b6e6eed00f2ad9418ef960 Mon Sep 17 00:00:00 2001 From: emmagarland Date: Sat, 6 Mar 2021 13:51:41 +0000 Subject: [PATCH 06/78] Added delete tests --- .../Security/MemberIdentityUserStoreTests.cs | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs index 8a0bf149b7..68e3f555ab 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs @@ -35,7 +35,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security { // arrange MemberUserStore sut = CreateSut(); - CancellationToken fakeCancellationToken = new CancellationToken(){}; + CancellationToken fakeCancellationToken = new CancellationToken() { }; // act Action actual = () => sut.CreateAsync(null, fakeCancellationToken); @@ -73,6 +73,66 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security // 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(); + var fakeUser = new MemberIdentityUser(777); + 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); + + // act + Action actual = () => sut.DeleteAsync(fakeUser, fakeCancellationToken); + + // assert + Assert.That(actual, Throws.ArgumentNullException); + _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(); } } } From df25e6ea531f379d705d307e217537834a317380 Mon Sep 17 00:00:00 2001 From: emmagarland Date: Sat, 6 Mar 2021 13:53:09 +0000 Subject: [PATCH 07/78] Fixed test --- .../Security/MemberIdentityUserStoreTests.cs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs index 68e3f555ab..a56b794ffb 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs @@ -83,20 +83,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security { // arrange MemberUserStore sut = CreateSut(); - var fakeUser = new MemberIdentityUser(777); - 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); // act - Action actual = () => sut.DeleteAsync(fakeUser, fakeCancellationToken); + Action actual = () => sut.DeleteAsync(null); // assert Assert.That(actual, Throws.ArgumentNullException); From fa684222e8c45ab97554b3cb8395c47583a70201 Mon Sep 17 00:00:00 2001 From: emmagarland Date: Sat, 6 Mar 2021 18:57:54 +0000 Subject: [PATCH 08/78] Updated unit tests --- .../Security/MemberUserStore.cs | 11 +- .../Security/MemberIdentityUserStoreTests.cs | 118 +++++++++++++++++- .../Security/MemberRoleStoreTests.cs | 24 ++-- 3 files changed, 131 insertions(+), 22 deletions(-) diff --git a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs index 1d871e4134..c0e4373826 100644 --- a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs @@ -21,6 +21,7 @@ namespace Umbraco.Cms.Core.Security /// 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; @@ -49,10 +50,10 @@ namespace Umbraco.Cms.Core.Security public override IQueryable Users => throw new NotImplementedException(); /// - public override Task GetNormalizedUserNameAsync(MemberIdentityUser user, CancellationToken cancellationToken) => GetUserNameAsync(user, cancellationToken); + public override Task GetNormalizedUserNameAsync(MemberIdentityUser user, CancellationToken cancellationToken = default) => GetUserNameAsync(user, cancellationToken); /// - public override Task SetNormalizedUserNameAsync(MemberIdentityUser 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(MemberIdentityUser user, CancellationToken cancellationToken = default) @@ -104,7 +105,7 @@ namespace Umbraco.Cms.Core.Security } catch (Exception ex) { - return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = ex.Message, Description = ex.Message })); + return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = genericIdentityErrorCode, Description = ex.Message })); } } @@ -160,7 +161,7 @@ namespace Umbraco.Cms.Core.Security } catch (Exception ex) { - return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = ex.Message, Description = ex.Message })); + return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = genericIdentityErrorCode, Description = ex.Message })); } } @@ -189,7 +190,7 @@ namespace Umbraco.Cms.Core.Security } catch (Exception ex) { - return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = ex.Message, Description = ex.Message })); + return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = genericIdentityErrorCode, Description = ex.Message })); } } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs index a56b794ffb..5d9303b908 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Data; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -31,19 +32,127 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security } [Test] - public void GivenICreateUser_AndTheUserIsNull_ThenIShouldGetAFailedResultAsync() + public void GivenIGetNormalizedUserName_AndTheUserIsNull_ThenIShouldGetAnException() { // arrange MemberUserStore sut = CreateSut(); CancellationToken fakeCancellationToken = new CancellationToken() { }; // act - Action actual = () => sut.CreateAsync(null, fakeCancellationToken); + 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(); + CancellationToken fakeCancellationToken = new CancellationToken() { }; + + // act + Action actual = () => sut.SetNormalizedUserNameAsync(null, "username", fakeCancellationToken); + + // assert + Assert.That(actual, Throws.ArgumentNullException); + } + + + [Test] + public void GivenISetNormalizedUserName_AndTheUserNameIsNull_ThenIShouldGetANull() + { + // arrange + MemberUserStore sut = CreateSut(); + CancellationToken fakeCancellationToken = new CancellationToken() { }; + var fakeUser = new MemberIdentityUser() { }; + + // act + Task actual = sut.SetNormalizedUserNameAsync(fakeUser, null, fakeCancellationToken); + + // assert + Assert.AreEqual(null, actual); + } + + [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() @@ -85,10 +194,11 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security MemberUserStore sut = CreateSut(); // act - Action actual = () => sut.DeleteAsync(null); + IdentityResult actual = await sut.DeleteAsync(null); // assert - Assert.That(actual, Throws.ArgumentNullException); + Assert.IsTrue(actual.Succeeded == false); + Assert.IsTrue(actual.Errors.Any(x => x.Code == "IdentityErrorUserStore" && x.Description == "Value cannot be null. (Parameter 'user')")); _mockMemberService.VerifyNoOtherCalls(); } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs index 3b78bc1b39..e6bb0b9ff5 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs @@ -26,17 +26,18 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security } [Test] - public void GivenICreateAMemberRole_AndTheGroupIsNull_ThenIShouldGetAnArgumentException() + public void GivenICreateAMemberRole_AndTheGroupIsNull_ThenIShouldGetAFailedIdentityResult() { // arrange MemberRoleStore sut = CreateSut(); CancellationToken fakeCancellationToken = new CancellationToken() { }; // act - Action actual = () => sut.CreateAsync(null, fakeCancellationToken); + Task actual = sut.CreateAsync(null, fakeCancellationToken); // assert - Assert.That(actual, Throws.ArgumentNullException); + Assert.IsTrue(actual.Result.Succeeded == false); + Assert.IsTrue(actual.Result.Errors.Any(x => x.Code == "IdentityMemberGroupNotFound" && x.Description == "Member group not found")); } @@ -139,15 +140,12 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security }; var fakeCancellationToken = new CancellationToken() { }; - bool raiseEvents = false; - - // act IdentityResult identityResult = await sut.UpdateAsync(fakeRole, fakeCancellationToken); // assert Assert.IsTrue(identityResult.Succeeded == false); - Assert.IsTrue(identityResult.Errors.Any(x => x.Code == "InvalidRoleName" && x.Description == "Role name 'testname' is invalid.")); + Assert.IsTrue(identityResult.Errors.Any(x => x.Code == "IdentityMemberGroupNotFound" && x.Description == "Member group not found")); _mockMemberGroupService.Verify(x => x.GetById(777)); } @@ -168,7 +166,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security // assert Assert.IsTrue(identityResult.Succeeded == false); - Assert.IsTrue(identityResult.Errors.Any(x => x.Code == "DefaultError" && x.Description == "An unknown failure has occurred.")); + Assert.IsTrue(identityResult.Errors.Any(x => x.Code == "IdentityIdParseError" && x.Description == "Cannot parse ID to int")); _mockMemberGroupService.VerifyNoOtherCalls(); } @@ -221,7 +219,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security // assert Assert.IsTrue(identityResult.Succeeded == false); - Assert.IsTrue(identityResult.Errors.Any(x => x.Code == "DefaultError" && x.Description == "An unknown failure has occurred.")); + Assert.IsTrue(identityResult.Errors.Any(x => x.Code == "IdentityIdParseError" && x.Description == "Cannot parse ID to int")); _mockMemberGroupService.VerifyNoOtherCalls(); } @@ -247,7 +245,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security // assert Assert.IsTrue(identityResult.Succeeded == false); - Assert.IsTrue(identityResult.Errors.Any(x => x.Code == "InvalidRoleName" && x.Description == "Role name 'testname' is invalid.")); + Assert.IsTrue(identityResult.Errors.Any(x => x.Code == "IdentityMemberGroupNotFound" && x.Description == "Member group not found")); _mockMemberGroupService.Verify(x => x.GetById(777)); _mockMemberGroupService.VerifyNoOtherCalls(); } @@ -283,7 +281,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security } [Test] - public void GivenIFindAMemberRoleByRoleId_AndIdCannotBeParsedToAnInt_ThenIShouldGetAFailureResultAsync() + public async Task GivenIFindAMemberRoleByRoleId_AndIdCannotBeParsedToAnInt_ThenIShouldGetAFailureResultAsync() { // arrange MemberRoleStore sut = CreateSut(); @@ -295,10 +293,10 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security var fakeCancellationToken = new CancellationToken() { }; // act - Task actual = sut.FindByIdAsync(fakeRole.Id, fakeCancellationToken); + Action actual = () => sut.FindByIdAsync(fakeRole.Id, fakeCancellationToken); // assert - Assert.IsNull(actual); + Assert.That(actual, Throws.TypeOf()); _mockMemberGroupService.VerifyNoOtherCalls(); } From 4e9423687cceefc40b3732885240dc937c6ffd9c Mon Sep 17 00:00:00 2001 From: emmagarland Date: Sat, 6 Mar 2021 23:09:19 +0000 Subject: [PATCH 09/78] Changed controller to use role manager. Updated mapping. Don't commit test database. --- .gitignore | 1 + .../Security/MemberRoleStore.cs | 17 +++- .../Security/MemberRoleStoreTests.cs | 73 ++++++++++++++- .../Controllers/MemberController.cs | 1 + .../Controllers/MemberGroupController.cs | 90 +++++++++++++------ .../Mapping/MemberMapDefinition.cs | 16 ++++ 6 files changed, 164 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index f8a2ce511f..8aa2769368 100644 --- a/.gitignore +++ b/.gitignore @@ -202,3 +202,4 @@ 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.Tests.Integration/DatabaseContextTests.sdf diff --git a/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs b/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs index 4477c4870c..466b2435f6 100644 --- a/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs @@ -246,12 +246,25 @@ namespace Umbraco.Cms.Core.Security 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)) { - throw new ArgumentOutOfRangeException(nameof(roleId), $"{nameof(roleId)} is not a valid Int"); + 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); } - IMemberGroup memberGroup = _memberGroupService.GetById(id); return Task.FromResult(memberGroup == null ? null : MapFromMemberGroup(memberGroup)); } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs index e6bb0b9ff5..d89bda494c 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs @@ -251,7 +251,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security } [Test] - public async Task GivenIFindAMemberRoleByRoleId_AndRoleIdExists_ThenIShouldGetASuccessResultAsync() + public async Task GivenIFindAMemberRoleByRoleKey_AndRoleKeyExists_ThenIShouldGetASuccessResultAsync() { // arrange MemberRoleStore sut = CreateSut(); @@ -265,7 +265,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security { Name = "fakeGroupName", CreatorId = 123, - Id = 777 + Id = 777, + Key = Guid.NewGuid() }; _mockMemberGroupService.Setup(x => x.GetById(fakeRoleId)).Returns(fakeMemberGroup); @@ -281,7 +282,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security } [Test] - public async Task GivenIFindAMemberRoleByRoleId_AndIdCannotBeParsedToAnInt_ThenIShouldGetAFailureResultAsync() + public async Task GivenIFindAMemberRoleByRoleId_AndIdCannotBeParsedToAnIntOrGuid_ThenIShouldGetAFailureResultAsync() { // arrange MemberRoleStore sut = CreateSut(); @@ -300,6 +301,72 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security _mockMemberGroupService.VerifyNoOtherCalls(); } + [Test] + public async Task GivenIFindAMemberRoleByRoleId_AndIdCannotBeParsedToAnIntButCanBeToGuid_ThenIShouldGetASuccessResultAsync() + { + // arrange + MemberRoleStore sut = CreateSut(); + var fakeRole = new IdentityRole("fakeGroupName") + { + Id = "777" + }; + + var fakeRoleGuid = Guid.NewGuid(); + + IMemberGroup fakeMemberGroup = new MemberGroup() + { + Name = "fakeGroupName", + CreatorId = 123, + Id = 777, + Key = fakeRoleGuid + }; + + _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(); + var fakeRole = new IdentityRole("fakeGroupName") + { + Id = "777" + }; + + var fakeRoleId = 777; + + IMemberGroup fakeMemberGroup = new MemberGroup() + { + Name = "fakeGroupName", + CreatorId = 123, + Id = 777, + Key = Guid.NewGuid() + }; + + _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() { diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index b5f8674f9e..4a24cf1377 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -190,6 +190,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [OutgoingEditorModelEvent] public MemberDisplay GetByKey(Guid key) { + //TODO: convert to identity 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 df76736de9..f083f1e1b5 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs @@ -48,15 +48,23 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// /// - public ActionResult GetById(int id) + public async Task> GetById(int id) { - IdentityRole memberGroup = _roleManager.FindByIdAsync(id.ToString()).Result; + //TODO: did we envisage this - combination of service and identity manager? + IdentityRole identityRole = await _roleManager.FindByIdAsync(id.ToString()); + if (identityRole == null) + { + return NotFound(); + } + + IMemberGroup memberGroup = _memberGroupService.GetById(id); if (memberGroup == null) { return NotFound(); } - MemberGroupDisplay dto = _umbracoMapper.Map, MemberGroupDisplay>(memberGroup); + //TODO: the default identity role doesn't have all the properties IMemberGroup had, e.g. CreatorId + MemberGroupDisplay dto = _umbracoMapper.Map(memberGroup); return dto; } @@ -66,15 +74,21 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// /// - public ActionResult GetById(Guid id) + public async Task> GetById(Guid id) { - IMemberGroup memberGroup = _memberGroupService.GetById(id); - if (memberGroup == null) + //TODO: did we envisage just identity or a combination of service and identity manager? + IdentityRole identityRole = await _roleManager.FindByIdAsync(id.ToString()); + if (identityRole == null) { return NotFound(); } + //IMemberGroup memberGroup = _memberGroupService.GetById(id); + //if (memberGroup == null) + //{ + // return NotFound(); + //} - return _umbracoMapper.Map(memberGroup); + return _umbracoMapper.Map(identityRole); } /// @@ -82,7 +96,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// /// - public ActionResult GetById(Udi id) + public async Task> GetById(Udi id) { var guidUdi = id as GuidUdi; if (guidUdi == null) @@ -90,43 +104,53 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return NotFound(); } - IMemberGroup memberGroup = _memberGroupService.GetById(guidUdi.Guid); - if (memberGroup == null) + //TODO: can we do this via identity? + IdentityRole identityRole = await _roleManager.FindByIdAsync(id.ToString()); + if (identityRole == null) { return NotFound(); } - return _umbracoMapper.Map(memberGroup); + return _umbracoMapper.Map(identityRole); } - public IEnumerable GetByIds([FromQuery]int[] ids) + public async Task> GetByIds([FromQuery]int[] ids) { - var roles = new List>(); + var roles = new List(); foreach (int id in ids) { - Task role = _roleManager.FindByIdAsync(id.ToString()); - roles.Add(role.Result); + IdentityRole role = await _roleManager.FindByIdAsync(id.ToString()); + roles.Add(role); } - return roles.Select(x=> _umbracoMapper.Map, MemberGroupDisplay>(x)); + return roles.Select(x=> _umbracoMapper.Map(x)); } [HttpDelete] [HttpPost] - public IActionResult DeleteById(int id) + public async Task DeleteById(int id) { - IMemberGroup memberGroup = _memberGroupService.GetById(id); - if (memberGroup == null) + //TODO: are there any repercussions elsewhere for us changing these to async? + IdentityRole role = await _roleManager.FindByIdAsync(id.ToString()); + + if (role == null) { return NotFound(); } - _memberGroupService.Delete(memberGroup); - return Ok(); + IdentityResult roleDeleted = await _roleManager.DeleteAsync(role); + if (roleDeleted.Succeeded) + { + return Ok(); + } + else + { + return Problem("Issue during deletion - please see logs"); + } } - public IEnumerable GetAllGroups() => _roleManager.Roles.Select(x => _umbracoMapper.Map, MemberGroupDisplay>(x)); + public IEnumerable GetAllGroups() => _roleManager.Roles.Select(x => _umbracoMapper.Map(x)); public MemberGroupDisplay GetEmpty() { @@ -134,20 +158,28 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return _umbracoMapper.Map(item); } - public ActionResult PostSave(MemberGroupSave saveModel) + public async Task> PostSave(MemberGroupSave saveModel) { + int id = int.Parse(saveModel.Id.ToString()); - var id = int.Parse(saveModel.Id.ToString()); - var memberGroup = id > 0 ? _memberGroupService.GetById(id) : new MemberGroup(); - if (memberGroup == null) + IdentityRole role = id > 0 ? await _roleManager.FindByIdAsync(saveModel.Id.ToString()) : null; + + if (role == null) { return NotFound(); } - memberGroup.Name = saveModel.Name; - _memberGroupService.Save(memberGroup); + role.Name = saveModel.Name; + IdentityResult updatedResult = await _roleManager.UpdateAsync(role); - var display = _umbracoMapper.Map(memberGroup); + if (!updatedResult.Succeeded) + { + //TODO: what to retrun if there is a failed identity result + return Problem(); + } + + //TODO: should we return the identity role or return the group from the service? + MemberGroupDisplay display = _umbracoMapper.Map(role); display.AddSuccessNotification( _localizedTextService.Localize("speechBubbles/memberGroupSavedHeader"), diff --git a/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs index e88503794e..7ee456a323 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; @@ -33,6 +35,7 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping mapper.Define((source, context) => new MemberDisplay(), Map); mapper.Define((source, context) => new MemberBasic(), Map); mapper.Define((source, context) => new MemberGroupDisplay(), Map); + mapper.Define((source, context) => new MemberGroupDisplay(), Map); mapper.Define((source, context) => new ContentPropertyCollectionDto(), Map); } @@ -100,5 +103,18 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping { target.Properties = context.MapEnumerable(source.Properties); } + + /// + /// Maps an identity role to a member group display + /// + /// + /// + /// + private void Map(IdentityRole source, MemberGroupDisplay target, MapperContext context) + { + //TODO: this is all that is mapped at this time, we're losing a lot of properties + target.Id = source.Id; + target.Name = source.Name; + } } } From 5051f8160ff1955ecb19c4c5c73d6ffb8d8316b9 Mon Sep 17 00:00:00 2001 From: emmagarland Date: Sat, 6 Mar 2021 23:13:15 +0000 Subject: [PATCH 10/78] Remove unecessary check for int --- src/Umbraco.Infrastructure/Security/MemberUserStore.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs index c0e4373826..6be981b3d1 100644 --- a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs @@ -208,11 +208,6 @@ namespace Umbraco.Cms.Core.Security throw new ArgumentNullException(nameof(userId)); } - if (!Guid.TryParse(userId, out Guid id)) - { - throw new ArgumentOutOfRangeException(nameof(userId), $"{nameof(userId)} is not a valid x"); - } - IMember user = _memberService.GetById(UserIdToInt(userId)); if (user == null) { From 401f713ca522c01a08714089f45733164bcc8538 Mon Sep 17 00:00:00 2001 From: emmagarland Date: Sun, 7 Mar 2021 15:21:49 +0000 Subject: [PATCH 11/78] Updated logic in member group controller to ensure group is saved when new --- .../Controllers/MemberGroupController.cs | 44 ++++++++++++------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs index f083f1e1b5..cc712ca7c4 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs @@ -29,7 +29,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private readonly UmbracoMapper _umbracoMapper; private readonly ILocalizedTextService _localizedTextService; private readonly RoleManager _roleManager; - + public MemberGroupController( IMemberGroupService memberGroupService, UmbracoMapper umbracoMapper, @@ -114,7 +114,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return _umbracoMapper.Map(identityRole); } - public async Task> GetByIds([FromQuery]int[] ids) + public async Task> GetByIds([FromQuery] int[] ids) { var roles = new List(); @@ -124,7 +124,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers roles.Add(role); } - return roles.Select(x=> _umbracoMapper.Map(x)); + return roles.Select(x => _umbracoMapper.Map(x)); } [HttpDelete] @@ -158,25 +158,39 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return _umbracoMapper.Map(item); } + /// + /// Saves the member group via the identity role + /// If new, creates a new role, else updates the existing role + /// + /// + /// public async Task> PostSave(MemberGroupSave saveModel) { int id = int.Parse(saveModel.Id.ToString()); - IdentityRole role = id > 0 ? await _roleManager.FindByIdAsync(saveModel.Id.ToString()) : null; - - if (role == null) + IdentityRole role; + if (id > 0) { - return NotFound(); + role = await _roleManager.FindByIdAsync(saveModel.Id.ToString()); + role.Name = saveModel.Name; + IdentityResult updatedResult = await _roleManager.UpdateAsync(role); + if (!updatedResult.Succeeded) + { + //TODO: what to retrun if there is a failed identity result + return Problem(); + } + } + else + { + role = new IdentityRole(saveModel.Name); + IdentityResult updatedResult = await _roleManager.CreateAsync(role); } - role.Name = saveModel.Name; - IdentityResult updatedResult = await _roleManager.UpdateAsync(role); - - if (!updatedResult.Succeeded) - { - //TODO: what to retrun if there is a failed identity result - return Problem(); - } + //TODO: do we need to refetch the role? + //if (role == null) + //{ + // return NotFound(); + //} //TODO: should we return the identity role or return the group from the service? MemberGroupDisplay display = _umbracoMapper.Map(role); From a53688c5030a116ae5e6075d81c3d64b8afafdea Mon Sep 17 00:00:00 2001 From: emmagarland Date: Sun, 7 Mar 2021 15:58:11 +0000 Subject: [PATCH 12/78] Fixed logic for groups. --- .../Controllers/MemberGroupController.cs | 88 +++++++++++-------- 1 file changed, 53 insertions(+), 35 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs index cc712ca7c4..f90bd6458c 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs @@ -43,6 +43,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); } + //TODO: are there any repercussions elsewhere for us changing these to async? + /// /// Gets the member group json for the member group id /// @@ -68,7 +70,6 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return dto; } - /// /// Gets the member group json for the member group guid /// @@ -77,18 +78,18 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers public async Task> GetById(Guid id) { //TODO: did we envisage just identity or a combination of service and identity manager? - IdentityRole identityRole = await _roleManager.FindByIdAsync(id.ToString()); - if (identityRole == null) - { - return NotFound(); - } - //IMemberGroup memberGroup = _memberGroupService.GetById(id); - //if (memberGroup == null) + //IdentityRole identityRole = await _roleManager.FindByIdAsync(id.ToString()); + //if (identityRole == null) //{ // return NotFound(); //} + IMemberGroup memberGroup = _memberGroupService.GetById(id); + if (memberGroup == null) + { + return NotFound(); + } - return _umbracoMapper.Map(identityRole); + return _umbracoMapper.Map(memberGroup); } /// @@ -111,27 +112,36 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return NotFound(); } - return _umbracoMapper.Map(identityRole); - } - - public async Task> GetByIds([FromQuery] int[] ids) - { - var roles = new List(); - - foreach (int id in ids) + IMemberGroup memberGroup = _memberGroupService.GetById(guidUdi.Guid); + if (memberGroup == null) { - IdentityRole role = await _roleManager.FindByIdAsync(id.ToString()); - roles.Add(role); + return NotFound(); } - return roles.Select(x => _umbracoMapper.Map(x)); + return _umbracoMapper.Map(memberGroup); + } + + public IEnumerable GetByIds([FromQuery] int[] ids) + { + //var roles = new List(); + + //foreach (int id in ids) + //{ + // IdentityRole role = await _roleManager.FindByIdAsync(id.ToString()); + // roles.Add(role); + //} + + //return roles.Select(x => _umbracoMapper.Map(x)); + //TODO: does this need to be done via identity? + + return _memberGroupService.GetByIds(ids) + .Select(_umbracoMapper.Map); } [HttpDelete] [HttpPost] public async Task DeleteById(int id) { - //TODO: are there any repercussions elsewhere for us changing these to async? IdentityRole role = await _roleManager.FindByIdAsync(id.ToString()); if (role == null) @@ -150,7 +160,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } } - public IEnumerable GetAllGroups() => _roleManager.Roles.Select(x => _umbracoMapper.Map(x)); + //TODO: we don't currently implement IQueryableRoleStore, still using original service + //public IEnumerable GetAllGroups() => _roleManager.Roles.Select(x => _umbracoMapper.Map(x)); + public IEnumerable GetAllGroups() => _memberGroupService.GetAll().Select(x => _umbracoMapper.Map(x)); public MemberGroupDisplay GetEmpty() { @@ -169,31 +181,37 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers int id = int.Parse(saveModel.Id.ToString()); IdentityRole role; + IdentityResult updatedResult; if (id > 0) { role = await _roleManager.FindByIdAsync(saveModel.Id.ToString()); role.Name = saveModel.Name; - IdentityResult updatedResult = await _roleManager.UpdateAsync(role); - if (!updatedResult.Succeeded) - { - //TODO: what to retrun if there is a failed identity result - return Problem(); - } + updatedResult = await _roleManager.UpdateAsync(role); + } else { role = new IdentityRole(saveModel.Name); - IdentityResult updatedResult = await _roleManager.CreateAsync(role); + updatedResult = await _roleManager.CreateAsync(role); } - //TODO: do we need to refetch the role? - //if (role == null) - //{ - // return NotFound(); - //} + if (!updatedResult.Succeeded) + { + //TODO: what to return if there is a failed identity result + return Problem(); + } + + //TODO: do we need to refetch the member group? + int roleId = int.Parse(role.Id); + IMemberGroup memberGroup = _memberGroupService.GetById(roleId); + + if (memberGroup == null) + { + return NotFound(); + } //TODO: should we return the identity role or return the group from the service? - MemberGroupDisplay display = _umbracoMapper.Map(role); + MemberGroupDisplay display = _umbracoMapper.Map(memberGroup); display.AddSuccessNotification( _localizedTextService.Localize("speechBubbles/memberGroupSavedHeader"), From 1df78c1321fa31cec091240d63e77f2af2e087f0 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Mon, 8 Mar 2021 17:12:30 +0000 Subject: [PATCH 13/78] Updates to unit store tests and managing null username in members user store --- .../Security/MemberRoleStore.cs | 3 ++- .../Security/MemberUserStore.cs | 25 ++++++++++++++++++- .../Security/MemberIdentityUserStoreTests.cs | 14 +++++++---- .../Security/MemberRoleStoreTests.cs | 6 ++--- 4 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs b/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs index 466b2435f6..9b0618e8dd 100644 --- a/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs @@ -19,6 +19,7 @@ namespace Umbraco.Cms.Core.Security //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) { @@ -57,7 +58,7 @@ namespace Umbraco.Cms.Core.Security } catch (Exception ex) { - return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = ex.Message, Description = ex.Message })); + return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = genericIdentityErrorCode, Description = ex.Message })); } } diff --git a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs index 6be981b3d1..233f053333 100644 --- a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs @@ -53,7 +53,30 @@ namespace Umbraco.Cms.Core.Security public override Task GetNormalizedUserNameAsync(MemberIdentityUser user, CancellationToken cancellationToken = default) => GetUserNameAsync(user, cancellationToken); /// - public override Task SetNormalizedUserNameAsync(MemberIdentityUser user, string normalizedName, CancellationToken cancellationToken = default) => SetUserNameAsync(user, normalizedName, cancellationToken); + public override Task SetNormalizedUserNameAsync(MemberIdentityUser user, string normalizedName, + CancellationToken cancellationToken = default) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (normalizedName == null) + { + throw new ArgumentNullException(nameof(normalizedName)); + } + + return SetUserNameAsync(user, normalizedName, cancellationToken); + } + catch (Exception ex) + { + return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = genericIdentityErrorCode, Description = ex.Message })); + } + } /// public override Task CreateAsync(MemberIdentityUser user, CancellationToken cancellationToken = default) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs index 5d9303b908..de8ba1e36b 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs @@ -67,13 +67,15 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security { // arrange MemberUserStore sut = CreateSut(); - CancellationToken fakeCancellationToken = new CancellationToken() { }; + var fakeCancellationToken = new CancellationToken() { }; // act - Action actual = () => sut.SetNormalizedUserNameAsync(null, "username", fakeCancellationToken); + var actual = (Task)sut.SetNormalizedUserNameAsync(null, "username", fakeCancellationToken); // assert - Assert.That(actual, Throws.ArgumentNullException); + Assert.IsFalse(actual.Result.Succeeded); + Assert.IsTrue(actual.Result.Errors.Any(x => x.Code == "IdentityErrorUserStore" && x.Description == "Value cannot be null. (Parameter 'user')")); + _mockMemberService.VerifyNoOtherCalls(); } @@ -86,10 +88,12 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security var fakeUser = new MemberIdentityUser() { }; // act - Task actual = sut.SetNormalizedUserNameAsync(fakeUser, null, fakeCancellationToken); + var actual = (Task)sut.SetNormalizedUserNameAsync(fakeUser, null, fakeCancellationToken); // assert - Assert.AreEqual(null, actual); + Assert.IsFalse(actual.Result.Succeeded); + Assert.IsTrue(actual.Result.Errors.Any(x => x.Code == "IdentityErrorUserStore" && x.Description == "Value cannot be null. (Parameter 'normalizedName')")); + _mockMemberService.VerifyNoOtherCalls(); } [Test] diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs index d89bda494c..2c3cb901fc 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs @@ -30,14 +30,14 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security { // arrange MemberRoleStore sut = CreateSut(); - CancellationToken fakeCancellationToken = new CancellationToken() { }; + var fakeCancellationToken = new CancellationToken(); // act Task actual = sut.CreateAsync(null, fakeCancellationToken); // assert - Assert.IsTrue(actual.Result.Succeeded == false); - Assert.IsTrue(actual.Result.Errors.Any(x => x.Code == "IdentityMemberGroupNotFound" && x.Description == "Member group not found")); + Assert.IsFalse(actual.Result.Succeeded); + Assert.IsTrue(actual.Result.Errors.Any(x => x.Code == "IdentityErrorUserStore" && x.Description == "Value cannot be null. (Parameter 'role')")); } From 1a7bc6aa757cd2002f3570be1b2ab0f7f16644fe Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Mon, 8 Mar 2021 17:26:53 +0000 Subject: [PATCH 14/78] Corrected formatting and removed unwanted commented out code --- .../ServiceCollectionExtensions.cs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs index b123a659c5..1ff044ed3f 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs @@ -63,24 +63,15 @@ namespace Umbraco.Extensions /// /// Adds the services required for using Members Identity /// - public static void AddMembersIdentity(this IServiceCollection services) - { + public static void AddMembersIdentity(this IServiceCollection services) => services.BuildMembersIdentity() .AddDefaultTokenProviders() .AddMemberManager() - //.AddRoles() .AddUserStore() .AddRoleStore>() .AddRoleValidator>() .AddRoleManager>(); - //services.AddScoped>( - // s => new UserClaimsPrincipalFactory( - // s.GetService(), - // s.GetService>(), - // s.GetService>())); - } - private static MemberIdentityBuilder BuildMembersIdentity(this IServiceCollection services) { // Services used by Umbraco members identity From eb74f74c91ba147dfa1decce0501be6c53a99e9b Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 9 Mar 2021 07:52:32 +0100 Subject: [PATCH 15/78] Replace static events with notifications in MemberService, PublicAccessService and UserService, and clean up unused components etc. --- .../Cache/DistributedCacheBinder_Handlers.cs | 90 ------ .../Cache/DistributedCacheHandler.cs | 64 +++++ .../Compose/AuditEventsComposer.cs | 7 - ...ponent.cs => AuditNotificationsHandler.cs} | 258 +++++++----------- .../Compose/NotificationsComponent.cs | 181 ------------ .../Compose/NotificationsComposer.cs | 41 ++- .../Events/UserNotificationsHandler.cs | 25 +- .../FileUploadPropertyEditor.cs | 10 +- .../ImageCropperPropertyEditor.cs | 10 +- .../PropertyEditorsComponent.cs | 56 ---- .../PropertyEditorsComposer.cs | 10 - .../Services/Implement/MemberService.cs | 89 ++---- .../Services/Implement/PublicAccessService.cs | 53 +--- .../Services/Implement/UserService.cs | 124 ++++----- .../AssignedMemberRolesNotification.cs | 10 + ...ssignedUserGroupPermissionsNotification.cs | 15 + .../ExportedMemberNotification.cs | 19 ++ .../Notifications/MemberRolesNotification.cs | 17 ++ .../RemovedMemberRolesNotification.cs | 10 + 19 files changed, 390 insertions(+), 699 deletions(-) create mode 100644 src/Umbraco.Infrastructure/Cache/DistributedCacheHandler.cs delete mode 100644 src/Umbraco.Infrastructure/Compose/AuditEventsComposer.cs rename src/Umbraco.Infrastructure/Compose/{AuditEventsComponent.cs => AuditNotificationsHandler.cs} (61%) delete mode 100644 src/Umbraco.Infrastructure/Compose/NotificationsComponent.cs delete mode 100644 src/Umbraco.Infrastructure/PropertyEditors/PropertyEditorsComponent.cs delete mode 100644 src/Umbraco.Infrastructure/PropertyEditors/PropertyEditorsComposer.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/AssignedMemberRolesNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/AssignedUserGroupPermissionsNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/ExportedMemberNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/MemberRolesNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/RemovedMemberRolesNotification.cs diff --git a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs index 1aa4906029..ae6b6f135b 100644 --- a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs +++ b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs @@ -49,18 +49,6 @@ namespace Umbraco.Cms.Core.Cache _logger.LogInformation("Initializing Umbraco internal event handlers for cache refreshing."); - // bind to user and user group events - Bind(() => UserService.SavedUserGroup += UserService_SavedUserGroup, - () => UserService.SavedUserGroup -= UserService_SavedUserGroup); - Bind(() => UserService.DeletedUserGroup += UserService_DeletedUserGroup, - () => UserService.DeletedUserGroup -= UserService_DeletedUserGroup); - Bind(() => UserService.SavedUser += UserService_SavedUser, - () => UserService.SavedUser -= UserService_SavedUser); - Bind(() => UserService.DeletedUser += UserService_DeletedUser, - () => UserService.DeletedUser -= UserService_DeletedUser); - Bind(() => UserService.UserGroupPermissionsAssigned += UserService_UserGroupPermissionsAssigned, - () => UserService.UserGroupPermissionsAssigned -= UserService_UserGroupPermissionsAssigned); - // bind to dictionary events Bind(() => LocalizationService.DeletedDictionaryItem += LocalizationService_DeletedDictionaryItem, () => LocalizationService.DeletedDictionaryItem -= LocalizationService_DeletedDictionaryItem); @@ -112,10 +100,6 @@ namespace Umbraco.Cms.Core.Cache () => MacroService.Deleted -= MacroService_Deleted); // bind to member events - Bind(() => MemberService.Saved += MemberService_Saved, - () => MemberService.Saved -= MemberService_Saved); - Bind(() => MemberService.Deleted += MemberService_Deleted, - () => MemberService.Deleted -= MemberService_Deleted); Bind(() => MemberGroupService.Saved += MemberGroupService_Saved, () => MemberGroupService.Saved -= MemberGroupService_Saved); Bind(() => MemberGroupService.Deleted += MemberGroupService_Deleted, @@ -135,12 +119,6 @@ namespace Umbraco.Cms.Core.Cache //Bind(() => ContentService.DeletedBlueprint += ContentService_DeletedBlueprint, // () => ContentService.DeletedBlueprint -= ContentService_DeletedBlueprint); - // bind to public access events - Bind(() => PublicAccessService.Saved += PublicAccessService_Saved, - () => PublicAccessService.Saved -= PublicAccessService_Saved); - Bind(() => PublicAccessService.Deleted += PublicAccessService_Deleted, - () => PublicAccessService.Deleted -= PublicAccessService_Deleted); - // bind to relation type events Bind(() => RelationService.SavedRelationType += RelationService_SavedRelationType, () => RelationService.SavedRelationType -= RelationService_SavedRelationType); @@ -148,21 +126,6 @@ namespace Umbraco.Cms.Core.Cache () => RelationService.DeletedRelationType -= RelationService_DeletedRelationType); } - #region PublicAccessService - - private void PublicAccessService_Saved(IPublicAccessService sender, SaveEventArgs e) - { - - _distributedCache.RefreshPublicAccess(); - } - - private void PublicAccessService_Deleted(IPublicAccessService sender, DeleteEventArgs e) - { - _distributedCache.RefreshPublicAccess(); - } - - #endregion - #region ContentService /// @@ -296,45 +259,6 @@ namespace Umbraco.Cms.Core.Cache #endregion - #region UserService - - private void UserService_UserGroupPermissionsAssigned(IUserService sender, SaveEventArgs e) - { - // TODO: Not sure if we need this yet depends if we start caching permissions - //var groupIds = e.SavedEntities.Select(x => x.UserGroupId).Distinct(); - //foreach (var groupId in groupIds) - //{ - // DistributedCache.Instance.RefreshUserGroupPermissionsCache(groupId); - //} - } - - private void UserService_SavedUser(IUserService sender, SaveEventArgs e) - { - foreach (var entity in e.SavedEntities) - _distributedCache.RefreshUserCache(entity.Id); - } - - private void UserService_DeletedUser(IUserService sender, DeleteEventArgs e) - { - foreach (var entity in e.DeletedEntities) - _distributedCache.RemoveUserCache(entity.Id); - } - - private void UserService_SavedUserGroup(IUserService sender, SaveEventArgs e) - { - foreach (var entity in e.SavedEntities) - _distributedCache.RefreshUserGroupCache(entity.UserGroup.Id); - } - - private void UserService_DeletedUserGroup(IUserService sender, DeleteEventArgs e) - { - - foreach (var entity in e.DeletedEntities) - _distributedCache.RemoveUserGroupCache(entity.Id); - } - - #endregion - #region FileService /// @@ -390,20 +314,6 @@ namespace Umbraco.Cms.Core.Cache #endregion - #region MemberService - - private void MemberService_Deleted(IMemberService sender, DeleteEventArgs e) - { - _distributedCache.RemoveMemberCache(e.DeletedEntities.ToArray()); - } - - private void MemberService_Saved(IMemberService sender, SaveEventArgs e) - { - _distributedCache.RefreshMemberCache(e.SavedEntities.ToArray()); - } - - #endregion - #region MemberGroupService private void MemberGroupService_Deleted(IMemberGroupService sender, DeleteEventArgs e) diff --git a/src/Umbraco.Infrastructure/Cache/DistributedCacheHandler.cs b/src/Umbraco.Infrastructure/Cache/DistributedCacheHandler.cs new file mode 100644 index 0000000000..648edaca68 --- /dev/null +++ b/src/Umbraco.Infrastructure/Cache/DistributedCacheHandler.cs @@ -0,0 +1,64 @@ +using System.Linq; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Infrastructure.Services.Notifications; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache +{ + internal class DistributedCacheHandler : + INotificationHandler>, + INotificationHandler>, + INotificationHandler>, + INotificationHandler>, + INotificationHandler>, + INotificationHandler>, + INotificationHandler>, + INotificationHandler> + { + private readonly DistributedCache _distributedCache; + + public DistributedCacheHandler(DistributedCache distributedCache) => _distributedCache = distributedCache; + + public void Handle(SavedNotification notification) => _distributedCache.RefreshMemberCache(notification.SavedEntities.ToArray()); + + public void Handle(DeletedNotification notification) => _distributedCache.RemoveMemberCache(notification.DeletedEntities.ToArray()); + + public void Handle(SavedNotification notification) + { + foreach (var entity in notification.SavedEntities) + { + _distributedCache.RefreshUserCache(entity.Id); + } + } + + public void Handle(DeletedNotification notification) + { + foreach (var entity in notification.DeletedEntities) + { + _distributedCache.RemoveUserCache(entity.Id); + } + } + + public void Handle(SavedNotification notification) + { + foreach (var entity in notification.SavedEntities) + { + _distributedCache.RefreshUserGroupCache(entity.Id); + } + } + + public void Handle(DeletedNotification notification) + { + foreach (var entity in notification.DeletedEntities) + { + _distributedCache.RemoveUserGroupCache(entity.Id); + } + } + + public void Handle(SavedNotification notification) => _distributedCache.RefreshPublicAccess(); + + public void Handle(DeletedNotification notification) => _distributedCache.RefreshPublicAccess(); + } +} diff --git a/src/Umbraco.Infrastructure/Compose/AuditEventsComposer.cs b/src/Umbraco.Infrastructure/Compose/AuditEventsComposer.cs deleted file mode 100644 index b6d5f60765..0000000000 --- a/src/Umbraco.Infrastructure/Compose/AuditEventsComposer.cs +++ /dev/null @@ -1,7 +0,0 @@ -using Umbraco.Cms.Core.Composing; - -namespace Umbraco.Cms.Core.Compose -{ - public sealed class AuditEventsComposer : ComponentComposer, ICoreComposer - { } -} diff --git a/src/Umbraco.Infrastructure/Compose/AuditEventsComponent.cs b/src/Umbraco.Infrastructure/Compose/AuditNotificationsHandler.cs similarity index 61% rename from src/Umbraco.Infrastructure/Compose/AuditEventsComponent.cs rename to src/Umbraco.Infrastructure/Compose/AuditNotificationsHandler.cs index 7c526d330c..f152dd16cf 100644 --- a/src/Umbraco.Infrastructure/Compose/AuditEventsComponent.cs +++ b/src/Umbraco.Infrastructure/Compose/AuditNotificationsHandler.cs @@ -2,7 +2,6 @@ using System; using System.Linq; using System.Text; using Microsoft.Extensions.Options; -using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; @@ -10,12 +9,21 @@ using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Net; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Services.Implement; +using Umbraco.Cms.Infrastructure.Services.Notifications; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Compose { - public sealed class AuditEventsComponent : IComponent + public sealed class AuditNotificationsHandler : + INotificationHandler>, + INotificationHandler>, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler>, + INotificationHandler>, + INotificationHandler>, + INotificationHandler { private readonly IAuditService _auditService; private readonly IUserService _userService; @@ -23,55 +31,26 @@ namespace Umbraco.Cms.Core.Compose private readonly IIpResolver _ipResolver; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; private readonly GlobalSettings _globalSettings; + private readonly IMemberService _memberService; - public AuditEventsComponent( + public AuditNotificationsHandler( IAuditService auditService, IUserService userService, IEntityService entityService, IIpResolver ipResolver, IOptions globalSettings, - IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IMemberService memberService) { _auditService = auditService; _userService = userService; _entityService = entityService; _ipResolver = ipResolver; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _memberService = memberService; _globalSettings = globalSettings.Value; } - public void Initialize() - { - UserService.SavedUserGroup += OnSavedUserGroupWithUsers; - - UserService.SavedUser += OnSavedUser; - UserService.DeletedUser += OnDeletedUser; - UserService.UserGroupPermissionsAssigned += UserGroupPermissionAssigned; - - MemberService.Saved += OnSavedMember; - MemberService.Deleted += OnDeletedMember; - MemberService.AssignedRoles += OnAssignedRoles; - MemberService.RemovedRoles += OnRemovedRoles; - MemberService.Exported += OnMemberExported; - } - - public void Terminate() - { - UserService.SavedUserGroup -= OnSavedUserGroupWithUsers; - - UserService.SavedUser -= OnSavedUser; - UserService.DeletedUser -= OnDeletedUser; - UserService.UserGroupPermissionsAssigned -= UserGroupPermissionAssigned; - - MemberService.Saved -= OnSavedMember; - MemberService.Deleted -= OnDeletedMember; - MemberService.AssignedRoles -= OnAssignedRoles; - MemberService.RemovedRoles -= OnRemovedRoles; - MemberService.Exported -= OnMemberExported; - } - - public static IUser UnknownUser(GlobalSettings globalSettings) => new User(globalSettings) { Id = Cms.Core.Constants.Security.UnknownUserId, Name = Cms.Core.Constants.Security.UnknownUserName, Email = "" }; - private IUser CurrentPerformingUser { get @@ -82,45 +61,48 @@ namespace Umbraco.Cms.Core.Compose } } - private IUser GetPerformingUser(int userId) - { - var found = userId >= 0 ? _userService.GetUserById(userId) : null; - return found ?? UnknownUser(_globalSettings); - } + public static IUser UnknownUser(GlobalSettings globalSettings) => new User(globalSettings) { Id = Cms.Core.Constants.Security.UnknownUserId, Name = Cms.Core.Constants.Security.UnknownUserName, Email = "" }; private string PerformingIp => _ipResolver.GetCurrentRequestIpAddress(); - private string FormatEmail(IMember member) - { - return member == null ? string.Empty : member.Email.IsNullOrWhiteSpace() ? "" : $"<{member.Email}>"; - } + private string FormatEmail(IMember member) => member == null ? string.Empty : member.Email.IsNullOrWhiteSpace() ? "" : $"<{member.Email}>"; - private string FormatEmail(IUser user) - { - return user == null ? string.Empty : user.Email.IsNullOrWhiteSpace() ? "" : $"<{user.Email}>"; - } + private string FormatEmail(IUser user) => user == null ? string.Empty : user.Email.IsNullOrWhiteSpace() ? "" : $"<{user.Email}>"; - private void OnRemovedRoles(IMemberService sender, RolesEventArgs args) + public void Handle(SavedNotification notification) { var performingUser = CurrentPerformingUser; - var roles = string.Join(", ", args.Roles); - var members = sender.GetAllMembers(args.MemberIds).ToDictionary(x => x.Id, x => x); - foreach (var id in args.MemberIds) + var members = notification.SavedEntities; + foreach (var member in members) { - members.TryGetValue(id, out var member); + var dp = string.Join(", ", ((Member)member).GetWereDirtyProperties()); + _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, DateTime.UtcNow, - -1, $"Member {id} \"{member?.Name ?? "(unknown)"}\" {FormatEmail(member)}", - "umbraco/member/roles/removed", $"roles modified, removed {roles}"); + -1, $"Member {member.Id} \"{member.Name}\" {FormatEmail(member)}", + "umbraco/member/save", $"updating {(string.IsNullOrWhiteSpace(dp) ? "(nothing)" : dp)}"); } } - private void OnAssignedRoles(IMemberService sender, RolesEventArgs args) + public void Handle(DeletedNotification notification) { var performingUser = CurrentPerformingUser; - var roles = string.Join(", ", args.Roles); - var members = sender.GetAllMembers(args.MemberIds).ToDictionary(x => x.Id, x => x); - foreach (var id in args.MemberIds) + var members = notification.DeletedEntities; + foreach (var member in members) + { + _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + DateTime.UtcNow, + -1, $"Member {member.Id} \"{member.Name}\" {FormatEmail(member)}", + "umbraco/member/delete", $"delete member id:{member.Id} \"{member.Name}\" {FormatEmail(member)}"); + } + } + + public void Handle(AssignedMemberRolesNotification notification) + { + var performingUser = CurrentPerformingUser; + var roles = string.Join(", ", notification.Roles); + var members = _memberService.GetAllMembers(notification.MemberIds).ToDictionary(x => x.Id, x => x); + foreach (var id in notification.MemberIds) { members.TryGetValue(id, out var member); _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, @@ -130,10 +112,25 @@ namespace Umbraco.Cms.Core.Compose } } - private void OnMemberExported(IMemberService sender, ExportedMemberEventArgs exportedMemberEventArgs) + public void Handle(RemovedMemberRolesNotification notification) { var performingUser = CurrentPerformingUser; - var member = exportedMemberEventArgs.Member; + var roles = string.Join(", ", notification.Roles); + var members = _memberService.GetAllMembers(notification.MemberIds).ToDictionary(x => x.Id, x => x); + foreach (var id in notification.MemberIds) + { + members.TryGetValue(id, out var member); + _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + DateTime.UtcNow, + -1, $"Member {id} \"{member?.Name ?? "(unknown)"}\" {FormatEmail(member)}", + "umbraco/member/roles/removed", $"roles modified, removed {roles}"); + } + } + + public void Handle(ExportedMemberNotification notification) + { + var performingUser = CurrentPerformingUser; + var member = notification.Member; _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, DateTime.UtcNow, @@ -141,10 +138,40 @@ namespace Umbraco.Cms.Core.Compose "umbraco/member/exported", "exported member data"); } - private void OnSavedUserGroupWithUsers(IUserService sender, SaveEventArgs saveEventArgs) + public void Handle(SavedNotification notification) { var performingUser = CurrentPerformingUser; - foreach (var groupWithUser in saveEventArgs.SavedEntities) + var affectedUsers = notification.SavedEntities; + foreach (var affectedUser in affectedUsers) + { + var groups = affectedUser.WasPropertyDirty("Groups") + ? string.Join(", ", affectedUser.Groups.Select(x => x.Alias)) + : null; + + var dp = string.Join(", ", ((User)affectedUser).GetWereDirtyProperties()); + + _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + DateTime.UtcNow, + affectedUser.Id, $"User \"{affectedUser.Name}\" {FormatEmail(affectedUser)}", + "umbraco/user/save", $"updating {(string.IsNullOrWhiteSpace(dp) ? "(nothing)" : dp)}{(groups == null ? "" : "; groups assigned: " + groups)}"); + } + } + + public void Handle(DeletedNotification notification) + { + var performingUser = CurrentPerformingUser; + var affectedUsers = notification.DeletedEntities; + foreach (var affectedUser in affectedUsers) + _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + DateTime.UtcNow, + affectedUser.Id, $"User \"{affectedUser.Name}\" {FormatEmail(affectedUser)}", + "umbraco/user/delete", "delete user"); + } + + public void Handle(SavedNotification notification) + { + var performingUser = CurrentPerformingUser; + foreach (var groupWithUser in notification.SavedEntities) { var group = groupWithUser.UserGroup; @@ -192,13 +219,13 @@ namespace Umbraco.Cms.Core.Compose } } - private void UserGroupPermissionAssigned(IUserService sender, SaveEventArgs saveEventArgs) + public void Handle(AssignedUserGroupPermissionsNotification notification) { var performingUser = CurrentPerformingUser; - var perms = saveEventArgs.SavedEntities; + var perms = notification.EntityPermissions; foreach (var perm in perms) { - var group = sender.GetUserGroupById(perm.UserGroupId); + var group = _userService.GetUserGroupById(perm.UserGroupId); var assigned = string.Join(", ", perm.AssignedPermissions); var entity = _entityService.Get(perm.EntityId); @@ -208,100 +235,5 @@ namespace Umbraco.Cms.Core.Compose "umbraco/user-group/permissions-change", $"assigning {(string.IsNullOrWhiteSpace(assigned) ? "(nothing)" : assigned)} on id:{perm.EntityId} \"{entity.Name}\""); } } - - private void OnSavedMember(IMemberService sender, SaveEventArgs saveEventArgs) - { - var performingUser = CurrentPerformingUser; - var members = saveEventArgs.SavedEntities; - foreach (var member in members) - { - var dp = string.Join(", ", ((Member) member).GetWereDirtyProperties()); - - _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, - DateTime.UtcNow, - -1, $"Member {member.Id} \"{member.Name}\" {FormatEmail(member)}", - "umbraco/member/save", $"updating {(string.IsNullOrWhiteSpace(dp) ? "(nothing)" : dp)}"); - } - } - - private void OnDeletedMember(IMemberService sender, DeleteEventArgs deleteEventArgs) - { - var performingUser = CurrentPerformingUser; - var members = deleteEventArgs.DeletedEntities; - foreach (var member in members) - { - _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, - DateTime.UtcNow, - -1, $"Member {member.Id} \"{member.Name}\" {FormatEmail(member)}", - "umbraco/member/delete", $"delete member id:{member.Id} \"{member.Name}\" {FormatEmail(member)}"); - } - } - - private void OnSavedUser(IUserService sender, SaveEventArgs saveEventArgs) - { - var performingUser = CurrentPerformingUser; - var affectedUsers = saveEventArgs.SavedEntities; - foreach (var affectedUser in affectedUsers) - { - var groups = affectedUser.WasPropertyDirty("Groups") - ? string.Join(", ", affectedUser.Groups.Select(x => x.Alias)) - : null; - - var dp = string.Join(", ", ((User)affectedUser).GetWereDirtyProperties()); - - _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, - DateTime.UtcNow, - affectedUser.Id, $"User \"{affectedUser.Name}\" {FormatEmail(affectedUser)}", - "umbraco/user/save", $"updating {(string.IsNullOrWhiteSpace(dp) ? "(nothing)" : dp)}{(groups == null ? "" : "; groups assigned: " + groups)}"); - } - } - - private void OnDeletedUser(IUserService sender, DeleteEventArgs deleteEventArgs) - { - var performingUser = CurrentPerformingUser; - var affectedUsers = deleteEventArgs.DeletedEntities; - foreach (var affectedUser in affectedUsers) - _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, - DateTime.UtcNow, - affectedUser.Id, $"User \"{affectedUser.Name}\" {FormatEmail(affectedUser)}", - "umbraco/user/delete", "delete user"); - } - - private void WriteAudit(int performingId, int affectedId, string ipAddress, string eventType, string eventDetails, string affectedDetails = null) - { - var performingUser = _userService.GetUserById(performingId); - - var performingDetails = performingUser == null - ? $"User UNKNOWN:{performingId}" - : $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}"; - - WriteAudit(performingId, performingDetails, affectedId, ipAddress, eventType, eventDetails, affectedDetails); - } - - private void WriteAudit(IUser performingUser, int affectedId, string ipAddress, string eventType, string eventDetails) - { - var performingDetails = performingUser == null - ? $"User UNKNOWN" - : $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}"; - - WriteAudit(performingUser?.Id ?? 0, performingDetails, affectedId, ipAddress, eventType, eventDetails); - } - - private void WriteAudit(int performingId, string performingDetails, int affectedId, string ipAddress, string eventType, string eventDetails, string affectedDetails = null) - { - if (affectedDetails == null) - { - var affectedUser = _userService.GetUserById(affectedId); - affectedDetails = affectedUser == null - ? $"User UNKNOWN:{affectedId}" - : $"User \"{affectedUser.Name}\" {FormatEmail(affectedUser)}"; - } - - _auditService.Write(performingId, performingDetails, - ipAddress, - DateTime.UtcNow, - affectedId, affectedDetails, - eventType, eventDetails); - } } } diff --git a/src/Umbraco.Infrastructure/Compose/NotificationsComponent.cs b/src/Umbraco.Infrastructure/Compose/NotificationsComponent.cs deleted file mode 100644 index 687fdbf294..0000000000 --- a/src/Umbraco.Infrastructure/Compose/NotificationsComponent.cs +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Umbraco.Cms.Core.Actions; -using Umbraco.Cms.Core.Composing; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.Membership; -using Umbraco.Cms.Core.Security; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Services.Implement; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Core.Compose -{ - /// - /// TODO: this component must be removed entirely - there is some code duplication in in anticipation of this component being deleted - /// - public sealed class NotificationsComponent : IComponent - { - private readonly Notifier _notifier; - private readonly ActionCollection _actions; - private readonly IContentService _contentService; - - public NotificationsComponent(Notifier notifier, ActionCollection actions, IContentService contentService) - { - _notifier = notifier; - _actions = actions; - _contentService = contentService; - } - - public void Initialize() - { - //Send notifications for the public access changed action - PublicAccessService.Saved += PublicAccessService_Saved; - - UserService.UserGroupPermissionsAssigned += UserService_UserGroupPermissionsAssigned; - } - - public void Terminate() - { - PublicAccessService.Saved -= PublicAccessService_Saved; - UserService.UserGroupPermissionsAssigned -= UserService_UserGroupPermissionsAssigned; - } - - private void UserService_UserGroupPermissionsAssigned(IUserService sender, SaveEventArgs args) - => UserServiceUserGroupPermissionsAssigned(args, _contentService); - - private void PublicAccessService_Saved(IPublicAccessService sender, SaveEventArgs args) - => PublicAccessServiceSaved(args, _contentService); - - private void UserServiceUserGroupPermissionsAssigned(SaveEventArgs args, IContentService contentService) - { - var entities = contentService.GetByIds(args.SavedEntities.Select(e => e.EntityId)).ToArray(); - if (entities.Any() == false) - { - return; - } - _notifier.Notify(_actions.GetAction(), entities); - } - - private void PublicAccessServiceSaved(SaveEventArgs args, IContentService contentService) - { - var entities = contentService.GetByIds(args.SavedEntities.Select(e => e.ProtectedNodeId)).ToArray(); - if (entities.Any() == false) - { - return; - } - _notifier.Notify(_actions.GetAction(), entities); - } - - /// - /// This class is used to send the notifications - /// - public sealed class Notifier - { - private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly INotificationService _notificationService; - private readonly IUserService _userService; - private readonly ILocalizedTextService _textService; - private readonly GlobalSettings _globalSettings; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - public Notifier( - IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - IHostingEnvironment hostingEnvironment, - INotificationService notificationService, - IUserService userService, - ILocalizedTextService textService, - IOptions globalSettings, - ILogger logger) - { - _backOfficeSecurityAccessor = backOfficeSecurityAccessor; - _hostingEnvironment = hostingEnvironment; - _notificationService = notificationService; - _userService = userService; - _textService = textService; - _globalSettings = globalSettings.Value; - _logger = logger; - } - - public void Notify(IAction action, params IContent[] entities) - { - var user = _backOfficeSecurityAccessor?.BackOfficeSecurity?.CurrentUser; - - //if there is no current user, then use the admin - if (user == null) - { - _logger.LogDebug("There is no current Umbraco user logged in, the notifications will be sent from the administrator"); - user = _userService.GetUserById(Constants.Security.SuperUserId); - if (user == null) - { - _logger.LogWarning("Notifications can not be sent, no admin user with id {SuperUserId} could be resolved", Constants.Security.SuperUserId); - return; - } - } - - SendNotification(user, entities, action, _hostingEnvironment.ApplicationMainUrl); - } - - private void SendNotification(IUser sender, IEnumerable entities, IAction action, Uri siteUri) - { - if (sender == null) throw new ArgumentNullException(nameof(sender)); - if (siteUri == null) - { - _logger.LogWarning("Notifications can not be sent, no site URL is set (might be during boot process?)"); - return; - } - - //group by the content type variation since the emails will be different - foreach(var contentVariantGroup in entities.GroupBy(x => x.ContentType.Variations)) - { - _notificationService.SendNotifications( - sender, - contentVariantGroup, - action.Letter.ToString(CultureInfo.InvariantCulture), - _textService.Localize("actions", action.Alias), - siteUri, - ((IUser user, NotificationEmailSubjectParams subject) x) - => _textService.Localize( - "notifications/mailSubject", - x.user.GetUserCulture(_textService, _globalSettings), - new[] { x.subject.SiteUrl, x.subject.Action, x.subject.ItemName }), - ((IUser user, NotificationEmailBodyParams body, bool isHtml) x) - => _textService.Localize( - x.isHtml ? "notifications/mailBodyHtml" : "notifications/mailBody", - x.user.GetUserCulture(_textService, _globalSettings), - new[] - { - x.body.RecipientName, - x.body.Action, - x.body.ItemName, - x.body.EditedUser, - x.body.SiteUrl, - x.body.ItemId, - //format the summary depending on if it's variant or not - contentVariantGroup.Key == ContentVariation.Culture - ? (x.isHtml ? _textService.Localize("notifications/mailBodyVariantHtmlSummary", new[]{ x.body.Summary }) : _textService.Localize("notifications/mailBodyVariantSummary", new []{ x.body.Summary })) - : x.body.Summary, - x.body.ItemUrl - })); - } - } - - } - } - - -} diff --git a/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs b/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs index 906f10c524..9b9253cfbf 100644 --- a/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs +++ b/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs @@ -1,10 +1,12 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Infrastructure.Services.Notifications; @@ -12,14 +14,10 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Core.Compose { - public sealed class NotificationsComposer : ComponentComposer, ICoreComposer + public sealed class NotificationsComposer : ICoreComposer { - public override void Compose(IUmbracoBuilder builder) + public void Compose(IUmbracoBuilder builder) { - base.Compose(builder); - - builder.Services.AddUnique(); - // add handlers for sending user notifications (i.e. emails) builder.Services.AddUnique(); builder @@ -31,7 +29,9 @@ namespace Umbraco.Cms.Core.Compose .AddNotificationHandler, UserNotificationsHandler>() .AddNotificationHandler, UserNotificationsHandler>() .AddNotificationHandler, UserNotificationsHandler>() - .AddNotificationHandler, UserNotificationsHandler>(); + .AddNotificationHandler, UserNotificationsHandler>() + .AddNotificationHandler() + .AddNotificationHandler, UserNotificationsHandler>(); // add handlers for building content relations builder @@ -51,10 +51,12 @@ namespace Umbraco.Cms.Core.Compose .AddNotificationHandler, FileUploadPropertyEditor>() .AddNotificationHandler, FileUploadPropertyEditor>() .AddNotificationHandler, FileUploadPropertyEditor>() + .AddNotificationHandler, FileUploadPropertyEditor>() .AddNotificationHandler, ImageCropperPropertyEditor>() .AddNotificationHandler, ImageCropperPropertyEditor>() .AddNotificationHandler, ImageCropperPropertyEditor>() - .AddNotificationHandler, ImageCropperPropertyEditor>(); + .AddNotificationHandler, ImageCropperPropertyEditor>() + .AddNotificationHandler, ImageCropperPropertyEditor>(); // add notification handlers for redirect tracking builder @@ -62,6 +64,29 @@ namespace Umbraco.Cms.Core.Compose .AddNotificationHandler, RedirectTrackingHandler>() .AddNotificationHandler, RedirectTrackingHandler>() .AddNotificationHandler, RedirectTrackingHandler>(); + + // add notification handlers for auditing + builder + .AddNotificationHandler, AuditNotificationsHandler>() + .AddNotificationHandler, AuditNotificationsHandler>() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler, AuditNotificationsHandler>() + .AddNotificationHandler, AuditNotificationsHandler>() + .AddNotificationHandler, AuditNotificationsHandler>() + .AddNotificationHandler(); + + // add notifications handlers for distributed cache + builder + .AddNotificationHandler, DistributedCacheHandler>() + .AddNotificationHandler, DistributedCacheHandler>() + .AddNotificationHandler, DistributedCacheHandler>() + .AddNotificationHandler, DistributedCacheHandler>() + .AddNotificationHandler, DistributedCacheHandler>() + .AddNotificationHandler, DistributedCacheHandler>() + .AddNotificationHandler, DistributedCacheHandler>() + .AddNotificationHandler, DistributedCacheHandler>(); } } } diff --git a/src/Umbraco.Infrastructure/Events/UserNotificationsHandler.cs b/src/Umbraco.Infrastructure/Events/UserNotificationsHandler.cs index a099a66d8e..ef5d9b660b 100644 --- a/src/Umbraco.Infrastructure/Events/UserNotificationsHandler.cs +++ b/src/Umbraco.Infrastructure/Events/UserNotificationsHandler.cs @@ -29,7 +29,9 @@ namespace Umbraco.Cms.Core.Events INotificationHandler>, INotificationHandler>, INotificationHandler>, - INotificationHandler> + INotificationHandler>, + INotificationHandler, + INotificationHandler> { private readonly Notifier _notifier; private readonly ActionCollection _actions; @@ -211,5 +213,26 @@ namespace Umbraco.Cms.Core.Events } } + + public void Handle(AssignedUserGroupPermissionsNotification notification) + { + var entities = _contentService.GetByIds(notification.EntityPermissions.Select(e => e.EntityId)).ToArray(); + if (entities.Any() == false) + { + return; + } + _notifier.Notify(_actions.GetAction(), entities); + + } + + public void Handle(SavedNotification notification) + { + var entities = _contentService.GetByIds(notification.SavedEntities.Select(e => e.ProtectedNodeId)).ToArray(); + if (entities.Any() == false) + { + return; + } + _notifier.Notify(_actions.GetAction(), entities); + } } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyEditor.cs index a43531bb83..6b75c54a12 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyEditor.cs @@ -27,7 +27,8 @@ namespace Umbraco.Cms.Core.PropertyEditors Icon = "icon-download-alt")] public class FileUploadPropertyEditor : DataEditor, IMediaUrlGenerator, INotificationHandler>, INotificationHandler>, - INotificationHandler>, INotificationHandler> + INotificationHandler>, INotificationHandler>, + INotificationHandler> { private readonly IMediaFileSystem _mediaFileSystem; private readonly ContentSettings _contentSettings; @@ -95,10 +96,7 @@ namespace Umbraco.Cms.Core.PropertyEditors /// The paths to all file upload property files contained within a collection of content entities /// /// - /// - /// This method must be made private once MemberService events have been replaced by notifications - /// - internal IEnumerable ContainedFilePaths(IEnumerable entities) => entities + private IEnumerable ContainedFilePaths(IEnumerable entities) => entities .SelectMany(x => x.Properties) .Where(IsUploadField) .SelectMany(GetFilePathsFromPropertyValues) @@ -162,6 +160,8 @@ namespace Umbraco.Cms.Core.PropertyEditors public void Handle(DeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities); + public void Handle(DeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities); + private void DeleteContainedFiles(IEnumerable deletedEntities) { var filePathsToDelete = ContainedFilePaths(deletedEntities); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs index d4c0c94054..886812d5a2 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs @@ -34,7 +34,8 @@ namespace Umbraco.Cms.Core.PropertyEditors Icon = "icon-crop")] public class ImageCropperPropertyEditor : DataEditor, IMediaUrlGenerator, INotificationHandler>, INotificationHandler>, - INotificationHandler>, INotificationHandler> + INotificationHandler>, INotificationHandler>, + INotificationHandler> { private readonly IMediaFileSystem _mediaFileSystem; private readonly ContentSettings _contentSettings; @@ -131,10 +132,7 @@ namespace Umbraco.Cms.Core.PropertyEditors /// The paths to all image cropper property files contained within a collection of content entities /// /// - /// - /// This method must be made private once MemberService events have been replaced by notifications - /// - internal IEnumerable ContainedFilePaths(IEnumerable entities) => entities + private IEnumerable ContainedFilePaths(IEnumerable entities) => entities .SelectMany(x => x.Properties) .Where(IsCropperField) .SelectMany(GetFilePathsFromPropertyValues) @@ -218,6 +216,8 @@ namespace Umbraco.Cms.Core.PropertyEditors public void Handle(DeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities); + public void Handle(DeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities); + private void DeleteContainedFiles(IEnumerable deletedEntities) { var filePathsToDelete = ContainedFilePaths(deletedEntities); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/PropertyEditorsComponent.cs b/src/Umbraco.Infrastructure/PropertyEditors/PropertyEditorsComponent.cs deleted file mode 100644 index b9e9e33889..0000000000 --- a/src/Umbraco.Infrastructure/PropertyEditors/PropertyEditorsComponent.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. - -using System; -using System.Collections.Generic; -using System.Linq; -using Umbraco.Cms.Core.Composing; -using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Services.Implement; - -namespace Umbraco.Cms.Core.PropertyEditors -{ - // TODO: delete this component and make the "ContainedFilePaths" methods on FileUploadPropertyEditor and ImageCropperPropertyEditor private once MemberService uses notifications instead of static events - public sealed class PropertyEditorsComponent : IComponent - { - private readonly PropertyEditorCollection _propertyEditors; - private readonly List _terminate = new List(); - - public PropertyEditorsComponent(PropertyEditorCollection propertyEditors) - { - _propertyEditors = propertyEditors; - } - - public void Initialize() - { - var fileUpload = _propertyEditors.OfType().FirstOrDefault(); - if (fileUpload != null) Initialize(fileUpload); - - var imageCropper = _propertyEditors.OfType().FirstOrDefault(); - if (imageCropper != null) Initialize(imageCropper); - - // grid/examine moved to ExamineComponent - } - - public void Terminate() - { - foreach (var t in _terminate) t(); - } - - private void Initialize(FileUploadPropertyEditor fileUpload) - { - void memberServiceDeleted(IMemberService sender, DeleteEventArgs args) => args.MediaFilesToDelete.AddRange(fileUpload.ContainedFilePaths(args.DeletedEntities.Cast())); - MemberService.Deleted += memberServiceDeleted; - _terminate.Add(() => MemberService.Deleted -= memberServiceDeleted); - } - - private void Initialize(ImageCropperPropertyEditor imageCropper) - { - void memberServiceDeleted(IMemberService sender, DeleteEventArgs args) => args.MediaFilesToDelete.AddRange(imageCropper.ContainedFilePaths(args.DeletedEntities.Cast())); - MemberService.Deleted += memberServiceDeleted; - _terminate.Add(() => MemberService.Deleted -= memberServiceDeleted); - } - } -} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/PropertyEditorsComposer.cs b/src/Umbraco.Infrastructure/PropertyEditors/PropertyEditorsComposer.cs deleted file mode 100644 index 4e876ad554..0000000000 --- a/src/Umbraco.Infrastructure/PropertyEditors/PropertyEditorsComposer.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. - -using Umbraco.Cms.Core.Composing; - -namespace Umbraco.Cms.Core.PropertyEditors -{ - public sealed class PropertyEditorsComposer : ComponentComposer, ICoreComposer - { } -} diff --git a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs index 96ba494790..026052fa67 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; @@ -9,6 +9,7 @@ using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Services.Notifications; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Services.Implement @@ -22,7 +23,6 @@ namespace Umbraco.Cms.Core.Services.Implement private readonly IMemberTypeRepository _memberTypeRepository; private readonly IMemberGroupRepository _memberGroupRepository; private readonly IAuditRepository _auditRepository; - private readonly IMemberTypeService _memberTypeService; private readonly IMemberGroupService _memberGroupService; @@ -773,10 +773,12 @@ namespace Umbraco.Cms.Core.Services.Implement member.Username = member.Username.Trim(); member.Email = member.Email.Trim(); + var evtMsgs = EventMessagesFactory.Get(); + using (IScope scope = ScopeProvider.CreateScope()) { - var saveEventArgs = new SaveEventArgs(member); - if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs)) + var savingNotification = new SavingNotification(member, evtMsgs); + if (raiseEvents && scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); return; @@ -793,8 +795,7 @@ namespace Umbraco.Cms.Core.Services.Implement if (raiseEvents) { - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(Saved, this, saveEventArgs); + scope.Notifications.Publish(new SavedNotification(member, evtMsgs).WithStateFrom(savingNotification)); } Audit(AuditType.Save, 0, member.Id); @@ -808,10 +809,12 @@ namespace Umbraco.Cms.Core.Services.Implement { var membersA = members.ToArray(); + var evtMsgs = EventMessagesFactory.Get(); + using (var scope = ScopeProvider.CreateScope()) { - var saveEventArgs = new SaveEventArgs(membersA); - if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs)) + var savingNotification = new SavingNotification(membersA, evtMsgs); + if (raiseEvents && scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); return; @@ -830,8 +833,7 @@ namespace Umbraco.Cms.Core.Services.Implement if (raiseEvents) { - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(Saved, this, saveEventArgs); + scope.Notifications.Publish(new SavedNotification(membersA, evtMsgs).WithStateFrom(savingNotification)); } Audit(AuditType.Save, 0, -1, "Save multiple Members"); @@ -849,32 +851,30 @@ namespace Umbraco.Cms.Core.Services.Implement /// to Delete public void Delete(IMember member) { + var evtMsgs = EventMessagesFactory.Get(); + using (var scope = ScopeProvider.CreateScope()) { - var deleteEventArgs = new DeleteEventArgs(member); - if (scope.Events.DispatchCancelable(Deleting, this, deleteEventArgs)) + var deletingNotification = new DeletingNotification(member, evtMsgs); + if (scope.Notifications.PublishCancelable(deletingNotification)) { scope.Complete(); return; } scope.WriteLock(Constants.Locks.MemberTree); - DeleteLocked(scope, member, deleteEventArgs); + DeleteLocked(scope, member, evtMsgs, deletingNotification.State); Audit(AuditType.Delete, 0, member.Id); scope.Complete(); } } - private void DeleteLocked(IScope scope, IMember member, DeleteEventArgs args = null) + private void DeleteLocked(IScope scope, IMember member, EventMessages evtMsgs, IDictionary notificationState = null) { // a member has no descendants _memberRepository.Delete(member); - if (args == null) - args = new DeleteEventArgs(member, false); // raise event & get flagged files - else - args.CanCancel = false; - scope.Events.Dispatch(Deleted, this, args); + scope.Notifications.Publish(new DeletedNotification(member, evtMsgs).WithState(notificationState)); // media files deleted by QueuingEventDispatcher } @@ -1017,8 +1017,7 @@ namespace Umbraco.Cms.Core.Services.Implement scope.WriteLock(Constants.Locks.MemberTree); int[] ids = _memberGroupRepository.GetMemberIds(usernames); _memberGroupRepository.AssignRoles(ids, roleNames); - scope.Events.Dispatch(AssignedRoles, this, new RolesEventArgs(ids, roleNames), nameof(AssignedRoles)); - scope.Complete(); + scope.Notifications.Publish(new AssignedMemberRolesNotification(ids, roleNames)); } } @@ -1031,8 +1030,7 @@ namespace Umbraco.Cms.Core.Services.Implement scope.WriteLock(Constants.Locks.MemberTree); int[] ids = _memberGroupRepository.GetMemberIds(usernames); _memberGroupRepository.DissociateRoles(ids, roleNames); - scope.Events.Dispatch(RemovedRoles, this, new RolesEventArgs(ids, roleNames), nameof(RemovedRoles)); - scope.Complete(); + scope.Notifications.Publish(new RemovedMemberRolesNotification(ids, roleNames)); } } @@ -1044,8 +1042,7 @@ namespace Umbraco.Cms.Core.Services.Implement { scope.WriteLock(Constants.Locks.MemberTree); _memberGroupRepository.AssignRoles(memberIds, roleNames); - scope.Events.Dispatch(AssignedRoles, this, new RolesEventArgs(memberIds, roleNames), nameof(AssignedRoles)); - scope.Complete(); + scope.Notifications.Publish(new AssignedMemberRolesNotification(memberIds, roleNames)); } } @@ -1057,8 +1054,7 @@ namespace Umbraco.Cms.Core.Services.Implement { scope.WriteLock(Constants.Locks.MemberTree); _memberGroupRepository.DissociateRoles(memberIds, roleNames); - scope.Events.Dispatch(RemovedRoles, this, new RolesEventArgs(memberIds, roleNames), nameof(RemovedRoles)); - scope.Complete(); + scope.Notifications.Publish(new RemovedMemberRolesNotification(memberIds, roleNames)); } } @@ -1072,36 +1068,6 @@ namespace Umbraco.Cms.Core.Services.Implement #region Event Handlers - /// - /// Occurs before Delete - /// - public static event TypedEventHandler> Deleting; - - /// - /// Occurs after Delete - /// - public static event TypedEventHandler> Deleted; - - /// - /// Occurs before Save - /// - public static event TypedEventHandler> Saving; - - /// - /// Occurs after Save - /// - public static event TypedEventHandler> Saved; - - /// - /// Occurs after roles have been assigned. - /// - public static event TypedEventHandler AssignedRoles; - - /// - /// Occurs after roles have been removed. - /// - public static event TypedEventHandler RemovedRoles; - /// /// Occurs after members have been exported. /// @@ -1145,7 +1111,7 @@ namespace Umbraco.Cms.Core.Services.Implement Properties = new List(GetPropertyExportItems(member)) }; - scope.Events.Dispatch(Exported, this, new ExportedMemberEventArgs(member, model)); + scope.Notifications.Publish(new ExportedMemberNotification(member, model)); return model; } @@ -1187,6 +1153,8 @@ namespace Umbraco.Cms.Core.Services.Implement /// Id of the MemberType public void DeleteMembersOfType(int memberTypeId) { + var evtMsgs = EventMessagesFactory.Get(); + // note: no tree to manage here using (IScope scope = ScopeProvider.CreateScope()) { @@ -1196,9 +1164,8 @@ namespace Umbraco.Cms.Core.Services.Implement IQuery query = Query().Where(x => x.ContentTypeId == memberTypeId); IMember[] members = _memberRepository.Get(query).ToArray(); - var deleteEventArgs = new DeleteEventArgs(members); - if (scope.Events.DispatchCancelable(Deleting, this, deleteEventArgs)) + if (scope.Notifications.PublishCancelable(new DeletingNotification(members, evtMsgs))) { scope.Complete(); return; @@ -1208,7 +1175,7 @@ namespace Umbraco.Cms.Core.Services.Implement { // delete media // triggers the deleted event (and handles the files) - DeleteLocked(scope, member); + DeleteLocked(scope, member, evtMsgs); } scope.Complete(); diff --git a/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs b/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs index 4c8615f442..b82f09d1b1 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; @@ -6,6 +6,7 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Services.Notifications; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Services.Implement @@ -129,8 +130,8 @@ namespace Umbraco.Cms.Core.Services.Implement return OperationResult.Attempt.Succeed(evtMsgs, entry); } - var saveEventArgs = new SaveEventArgs(entry, evtMsgs); - if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs)) + var savingNotifiation = new SavingNotification(entry, evtMsgs); + if (scope.Notifications.PublishCancelable(savingNotifiation)) { scope.Complete(); return OperationResult.Attempt.Cancel(evtMsgs, entry); @@ -140,8 +141,7 @@ namespace Umbraco.Cms.Core.Services.Implement scope.Complete(); - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(Saved, this, saveEventArgs); + scope.Notifications.Publish(new SavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); } return OperationResult.Attempt.Succeed(evtMsgs, entry); @@ -167,8 +167,8 @@ namespace Umbraco.Cms.Core.Services.Implement entry.RemoveRule(existingRule); - var saveEventArgs = new SaveEventArgs(entry, evtMsgs); - if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs)) + var savingNotifiation = new SavingNotification(entry, evtMsgs); + if (scope.Notifications.PublishCancelable(savingNotifiation)) { scope.Complete(); return OperationResult.Attempt.Cancel(evtMsgs); @@ -177,8 +177,7 @@ namespace Umbraco.Cms.Core.Services.Implement _publicAccessRepository.Save(entry); scope.Complete(); - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(Saved, this, saveEventArgs); + scope.Notifications.Publish(new SavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); } return OperationResult.Attempt.Succeed(evtMsgs); @@ -194,8 +193,8 @@ namespace Umbraco.Cms.Core.Services.Implement using (var scope = ScopeProvider.CreateScope()) { - var saveEventArgs = new SaveEventArgs(entry, evtMsgs); - if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs)) + var savingNotifiation = new SavingNotification(entry, evtMsgs); + if (scope.Notifications.PublishCancelable(savingNotifiation)) { scope.Complete(); return OperationResult.Attempt.Cancel(evtMsgs); @@ -204,8 +203,7 @@ namespace Umbraco.Cms.Core.Services.Implement _publicAccessRepository.Save(entry); scope.Complete(); - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(Saved, this, saveEventArgs); + scope.Notifications.Publish(new SavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); } return OperationResult.Attempt.Succeed(evtMsgs); @@ -221,8 +219,8 @@ namespace Umbraco.Cms.Core.Services.Implement using (var scope = ScopeProvider.CreateScope()) { - var deleteEventArgs = new DeleteEventArgs(entry, evtMsgs); - if (scope.Events.DispatchCancelable(Deleting, this, deleteEventArgs)) + var deletingNotification = new DeletingNotification(entry, evtMsgs); + if (scope.Notifications.PublishCancelable(deletingNotification)) { scope.Complete(); return OperationResult.Attempt.Cancel(evtMsgs); @@ -231,33 +229,10 @@ namespace Umbraco.Cms.Core.Services.Implement _publicAccessRepository.Delete(entry); scope.Complete(); - deleteEventArgs.CanCancel = false; - scope.Events.Dispatch(Deleted, this, deleteEventArgs); + scope.Notifications.Publish(new DeletedNotification(entry, evtMsgs).WithStateFrom(deletingNotification)); } return OperationResult.Attempt.Succeed(evtMsgs); } - - /// - /// Occurs before Save - /// - public static event TypedEventHandler> Saving; - - /// - /// Occurs after Save - /// - public static event TypedEventHandler> Saved; - - /// - /// Occurs before Delete - /// - public static event TypedEventHandler> Deleting; - - /// - /// Occurs after Delete - /// - public static event TypedEventHandler> Deleted; - - } } diff --git a/src/Umbraco.Infrastructure/Services/Implement/UserService.cs b/src/Umbraco.Infrastructure/Services/Implement/UserService.cs index 751581e068..3336ba37a3 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/UserService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/UserService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Data.Common; using System.Globalization; @@ -14,6 +14,7 @@ using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Services.Notifications; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Services.Implement @@ -109,6 +110,8 @@ namespace Umbraco.Cms.Core.Services.Implement if (username == null) throw new ArgumentNullException(nameof(username)); if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(username)); + var evtMsgs = EventMessagesFactory.Get(); + // TODO: PUT lock here!! User user; @@ -129,8 +132,8 @@ namespace Umbraco.Cms.Core.Services.Implement IsApproved = isApproved }; - var saveEventArgs = new SaveEventArgs(user); - if (scope.Events.DispatchCancelable(SavingUser, this, saveEventArgs)) + var savingNotification = new SavingNotification(user, evtMsgs); + if (scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); return user; @@ -138,8 +141,7 @@ namespace Umbraco.Cms.Core.Services.Implement _userRepository.Save(user); - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(SavedUser, this, saveEventArgs); + scope.Notifications.Publish(new SavedNotification(user, evtMsgs).WithStateFrom(savingNotification)); scope.Complete(); } @@ -239,10 +241,12 @@ namespace Umbraco.Cms.Core.Services.Implement } else { + var evtMsgs = EventMessagesFactory.Get(); + using (var scope = ScopeProvider.CreateScope()) { - var deleteEventArgs = new DeleteEventArgs(user); - if (scope.Events.DispatchCancelable(DeletingUser, this, deleteEventArgs)) + var deletingNotification = new DeletingNotification(user, evtMsgs); + if (scope.Notifications.PublishCancelable(deletingNotification)) { scope.Complete(); return; @@ -250,8 +254,7 @@ namespace Umbraco.Cms.Core.Services.Implement _userRepository.Delete(user); - deleteEventArgs.CanCancel = false; - scope.Events.Dispatch(DeletedUser, this, deleteEventArgs); + scope.Notifications.Publish(new DeletedNotification(user, evtMsgs).WithStateFrom(deletingNotification)); scope.Complete(); } } @@ -272,10 +275,12 @@ namespace Umbraco.Cms.Core.Services.Implement /// Default is True otherwise set to False to not raise events public void Save(IUser entity, bool raiseEvents = true) { + var evtMsgs = EventMessagesFactory.Get(); + using (var scope = ScopeProvider.CreateScope()) { - var saveEventArgs = new SaveEventArgs(entity); - if (raiseEvents && scope.Events.DispatchCancelable(SavingUser, this, saveEventArgs)) + var savingNotification = new SavingNotification(entity, evtMsgs); + if (raiseEvents && scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); return; @@ -292,8 +297,7 @@ namespace Umbraco.Cms.Core.Services.Implement _userRepository.Save(entity); if (raiseEvents) { - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(SavedUser, this, saveEventArgs); + scope.Notifications.Publish(new SavedNotification(entity, evtMsgs).WithStateFrom(savingNotification)); } scope.Complete(); @@ -319,12 +323,14 @@ namespace Umbraco.Cms.Core.Services.Implement /// Default is True otherwise set to False to not raise events public void Save(IEnumerable entities, bool raiseEvents = true) { + var evtMsgs = EventMessagesFactory.Get(); + var entitiesA = entities.ToArray(); using (var scope = ScopeProvider.CreateScope()) { - var saveEventArgs = new SaveEventArgs(entitiesA); - if (raiseEvents && scope.Events.DispatchCancelable(SavingUser, this, saveEventArgs)) + var savingNotification = new SavingNotification(entitiesA, evtMsgs); + if (raiseEvents && scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); return; @@ -344,8 +350,7 @@ namespace Umbraco.Cms.Core.Services.Implement if (raiseEvents) { - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(SavedUser, this, saveEventArgs); + scope.Notifications.Publish(new SavedNotification(entitiesA, evtMsgs).WithStateFrom(savingNotification)); } //commit the whole lot in one go @@ -709,14 +714,16 @@ namespace Umbraco.Cms.Core.Services.Implement if (entityIds.Length == 0) return; + var evtMsgs = EventMessagesFactory.Get(); + using (var scope = ScopeProvider.CreateScope()) { _userGroupRepository.ReplaceGroupPermissions(groupId, permissions, entityIds); scope.Complete(); var assigned = permissions.Select(p => p.ToString(CultureInfo.InvariantCulture)).ToArray(); - scope.Events.Dispatch(UserGroupPermissionsAssigned, this, - new SaveEventArgs(entityIds.Select(x => new EntityPermission(groupId, x, assigned)).ToArray(), false)); + var entityPermissions = entityIds.Select(x => new EntityPermission(groupId, x, assigned)).ToArray(); + scope.Notifications.Publish(new AssignedUserGroupPermissionsNotification(entityPermissions, evtMsgs)); } } @@ -731,14 +738,16 @@ namespace Umbraco.Cms.Core.Services.Implement if (entityIds.Length == 0) return; + var evtMsgs = EventMessagesFactory.Get(); + using (var scope = ScopeProvider.CreateScope()) { _userGroupRepository.AssignGroupPermission(groupId, permission, entityIds); scope.Complete(); var assigned = new[] { permission.ToString(CultureInfo.InvariantCulture) }; - scope.Events.Dispatch(UserGroupPermissionsAssigned, this, - new SaveEventArgs(entityIds.Select(x => new EntityPermission(groupId, x, assigned)).ToArray(), false)); + var entityPermissions = entityIds.Select(x => new EntityPermission(groupId, x, assigned)).ToArray(); + scope.Notifications.Publish(new AssignedUserGroupPermissionsNotification(entityPermissions, evtMsgs)); } } @@ -809,6 +818,8 @@ namespace Umbraco.Cms.Core.Services.Implement /// Default is True otherwise set to False to not raise events public void Save(IUserGroup userGroup, int[] userIds = null, bool raiseEvents = true) { + var evtMsgs = EventMessagesFactory.Get(); + using (var scope = ScopeProvider.CreateScope()) { // we need to figure out which users have been added / removed, for audit purposes @@ -826,9 +837,19 @@ namespace Umbraco.Cms.Core.Services.Implement removedUsers = groupIds.Except(userIds).Select(x => xGroupUsers[x]).Where(x => x.Id != 0).ToArray(); } - var saveEventArgs = new SaveEventArgs(new UserGroupWithUsers(userGroup, addedUsers, removedUsers)); + var userGroupWithUsers = new UserGroupWithUsers(userGroup, addedUsers, removedUsers); - if (raiseEvents && scope.Events.DispatchCancelable(SavingUserGroup, this, saveEventArgs)) + // this is the default/expected notification for the IUserGroup entity being saved + var savingNotification = new SavingNotification(userGroup, evtMsgs); + if (raiseEvents && scope.Notifications.PublishCancelable(savingNotification)) + { + scope.Complete(); + return; + } + + // this is an additional notification for special auditing + var savingUserGroupWithUsersNotification = new SavingNotification(userGroupWithUsers, evtMsgs); + if (raiseEvents && scope.Notifications.PublishCancelable(savingUserGroupWithUsersNotification)) { scope.Complete(); return; @@ -838,8 +859,8 @@ namespace Umbraco.Cms.Core.Services.Implement if (raiseEvents) { - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(SavedUserGroup, this, saveEventArgs); + scope.Notifications.Publish(new SavedNotification(userGroup, evtMsgs).WithStateFrom(savingNotification)); + scope.Notifications.Publish(new SavedNotification(userGroupWithUsers, evtMsgs).WithStateFrom(savingUserGroupWithUsersNotification)); } scope.Complete(); @@ -852,10 +873,12 @@ namespace Umbraco.Cms.Core.Services.Implement /// UserGroup to delete public void DeleteUserGroup(IUserGroup userGroup) { + var evtMsgs = EventMessagesFactory.Get(); + using (var scope = ScopeProvider.CreateScope()) { - var deleteEventArgs = new DeleteEventArgs(userGroup); - if (scope.Events.DispatchCancelable(DeletingUserGroup, this, deleteEventArgs)) + var deletingNotification = new DeletingNotification(userGroup, evtMsgs); + if (scope.Notifications.PublishCancelable(deletingNotification)) { scope.Complete(); return; @@ -863,8 +886,7 @@ namespace Umbraco.Cms.Core.Services.Implement _userGroupRepository.Delete(userGroup); - deleteEventArgs.CanCancel = false; - scope.Events.Dispatch(DeletedUserGroup, this, deleteEventArgs); + scope.Notifications.Publish(new DeletedNotification(userGroup, evtMsgs).WithStateFrom(deletingNotification)); scope.Complete(); } @@ -1144,49 +1166,5 @@ namespace Umbraco.Cms.Core.Services.Implement } #endregion - - /// - /// Occurs before Save - /// - public static event TypedEventHandler> SavingUser; - - /// - /// Occurs after Save - /// - public static event TypedEventHandler> SavedUser; - - /// - /// Occurs before Delete - /// - public static event TypedEventHandler> DeletingUser; - - /// - /// Occurs after Delete - /// - public static event TypedEventHandler> DeletedUser; - - /// - /// Occurs before Save - /// - public static event TypedEventHandler> SavingUserGroup; - - /// - /// Occurs after Save - /// - public static event TypedEventHandler> SavedUserGroup; - - /// - /// Occurs before Delete - /// - public static event TypedEventHandler> DeletingUserGroup; - - /// - /// Occurs after Delete - /// - public static event TypedEventHandler> DeletedUserGroup; - - // TODO: still don't know if we need this yet unless we start caching permissions, but that also means we'll need another - // event on the ContentService since there's a method there to modify node permissions too, or we can proxy events if needed. - public static event TypedEventHandler> UserGroupPermissionsAssigned; } } diff --git a/src/Umbraco.Infrastructure/Services/Notifications/AssignedMemberRolesNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/AssignedMemberRolesNotification.cs new file mode 100644 index 0000000000..9d5d707f64 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/AssignedMemberRolesNotification.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class AssignedMemberRolesNotification : MemberRolesNotification + { + public AssignedMemberRolesNotification(int[] memberIds, string[] roles) : base(memberIds, roles) + { + + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/AssignedUserGroupPermissionsNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/AssignedUserGroupPermissionsNotification.cs new file mode 100644 index 0000000000..e0838f8c33 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/AssignedUserGroupPermissionsNotification.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models.Membership; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class AssignedUserGroupPermissionsNotification : EnumerableObjectNotification + { + public AssignedUserGroupPermissionsNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + + public IEnumerable EntityPermissions => Target; + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/ExportedMemberNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/ExportedMemberNotification.cs new file mode 100644 index 0000000000..449244242f --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/ExportedMemberNotification.cs @@ -0,0 +1,19 @@ +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class ExportedMemberNotification : INotification + { + public ExportedMemberNotification(IMember member, MemberExportModel exported) + { + Member = member; + Exported = exported; + } + + public IMember Member { get; } + + public MemberExportModel Exported { get; } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/MemberRolesNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/MemberRolesNotification.cs new file mode 100644 index 0000000000..2b7c9bd828 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/MemberRolesNotification.cs @@ -0,0 +1,17 @@ +using Umbraco.Cms.Core.Events; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public abstract class MemberRolesNotification : INotification + { + protected MemberRolesNotification(int[] memberIds, string[] roles) + { + MemberIds = memberIds; + Roles = roles; + } + + public int[] MemberIds { get; } + + public string[] Roles { get; } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/RemovedMemberRolesNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/RemovedMemberRolesNotification.cs new file mode 100644 index 0000000000..bd902cb078 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/RemovedMemberRolesNotification.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class RemovedMemberRolesNotification : MemberRolesNotification + { + public RemovedMemberRolesNotification(int[] memberIds, string[] roles) : base(memberIds, roles) + { + + } + } +} From 239edaa9523993a2529ebf7ac7c6704a95d875f6 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 9 Mar 2021 08:01:19 +0100 Subject: [PATCH 16/78] Missing file commit --- .../Security/BackOfficeUserManagerAuditer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManagerAuditer.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManagerAuditer.cs index 6e67ee84ba..5af34d3b52 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManagerAuditer.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManagerAuditer.cs @@ -47,11 +47,11 @@ namespace Umbraco.Cms.Web.BackOffice.Security { if (!int.TryParse(userId, out int asInt)) { - return AuditEventsComponent.UnknownUser(_globalSettings); + return AuditNotificationsHandler.UnknownUser(_globalSettings); } IUser found = asInt >= 0 ? _userService.GetUserById(asInt) : null; - return found ?? AuditEventsComponent.UnknownUser(_globalSettings); + return found ?? AuditNotificationsHandler.UnknownUser(_globalSettings); } private static string FormatEmail(IMembershipUser user) => user == null ? string.Empty : user.Email.IsNullOrWhiteSpace() ? "" : $"<{user.Email}>"; From 4873ecc0f90b2c0da9c74df94d2e9564604b351a Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Tue, 9 Mar 2021 10:13:44 +0000 Subject: [PATCH 17/78] Deleted unwanted extra method --- .../Services/Implement/MemberGroupService.cs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/Umbraco.Infrastructure/Services/Implement/MemberGroupService.cs b/src/Umbraco.Infrastructure/Services/Implement/MemberGroupService.cs index 1f6d4743be..9024e3c4e1 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MemberGroupService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/MemberGroupService.cs @@ -130,25 +130,6 @@ namespace Umbraco.Cms.Core.Services.Implement } } - - public void GetByRole(IMemberGroup memberGroup) - { - using (var scope = ScopeProvider.CreateScope()) - { - var deleteEventArgs = new DeleteEventArgs(memberGroup); - if (scope.Events.DispatchCancelable(Deleting, this, deleteEventArgs)) - { - scope.Complete(); - return; - } - - _memberGroupRepository.Delete(memberGroup); - scope.Complete(); - deleteEventArgs.CanCancel = false; - scope.Events.Dispatch(Deleted, this, deleteEventArgs); - } - } - /// /// Occurs before Delete of a member group /// From 9e867eeeefddff13fa0295ecc8e2d2a711aeeb73 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Fri, 12 Mar 2021 15:05:50 +0000 Subject: [PATCH 18/78] Changes as per PR comments from Scott Brady --- .../Security/MemberRoleStore.cs | 36 +++---------------- .../Controllers/ContentController.cs | 2 +- .../Umbraco.Web.Common.csproj | 1 - 3 files changed, 5 insertions(+), 34 deletions(-) diff --git a/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs b/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs index 9b0618e8dd..d76f3b5c7b 100644 --- a/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs @@ -69,13 +69,13 @@ namespace Umbraco.Cms.Core.Security try { cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) { throw new ArgumentNullException(nameof(role)); } - ThrowIfDisposed(); - if (!int.TryParse(role.Id, out int roleId)) { return Task.FromResult(IdentityResult.Failed(_intParseError)); @@ -205,36 +205,10 @@ namespace Umbraco.Cms.Core.Security } /// - public Task GetNormalizedRoleNameAsync(TRole role, CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - - //TODO: are we utilising NormalizedRoleName? - return Task.FromResult(role.NormalizedName); - } + public Task GetNormalizedRoleNameAsync(TRole role, CancellationToken cancellationToken = default) => GetRoleNameAsync(role, cancellationToken); /// - public Task SetNormalizedRoleNameAsync(TRole role, string normalizedName, CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - - //TODO: are we utilising NormalizedRoleName and do we need to set it in the memberGroupService? - role.NormalizedName = normalizedName; - - return Task.CompletedTask; - } + public Task SetNormalizedRoleNameAsync(TRole role, string normalizedName, CancellationToken cancellationToken = default) => SetRoleNameAsync(role, normalizedName, cancellationToken); /// public Task FindByIdAsync(string roleId, CancellationToken cancellationToken = default) @@ -280,8 +254,6 @@ namespace Umbraco.Cms.Core.Security throw new ArgumentNullException(nameof(name)); } IMemberGroup memberGroup = _memberGroupService.GetByName(name); - //TODO: throw exception when not found? - return Task.FromResult(memberGroup == null ? null : MapFromMemberGroup(memberGroup)); } diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index 00f2fff56c..c4c601593f 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -2435,7 +2435,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers .Select(_umbracoMapper.Map) .ToArray(); - //TODO: change to role store + //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.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index 0fe68d4cc9..738a9389dc 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -24,7 +24,6 @@ - From 8535a2915c05a4b4ab084e3d23a85ee16ff1422c Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Fri, 12 Mar 2021 15:21:56 +0000 Subject: [PATCH 19/78] Changed wording, PR feedback --- .../Mapping/MemberTabsAndPropertiesMapper.cs | 2 +- .../Security/MemberUserStore.cs | 25 +------------------ 2 files changed, 2 insertions(+), 25 deletions(-) diff --git a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs index 9ac1b05d0f..02ee4fe744 100644 --- a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs @@ -237,7 +237,7 @@ namespace Umbraco.Cms.Core.Models.Mapping var userRoles = username.IsNullOrWhiteSpace() ? null : _memberService.GetAllRoles(username); // create a dictionary of all roles (except internal roles) + "false" - //TODO: use MembersRoleStore + //TODO: use member role manager instead var result = _memberGroupService.GetAll() .Select(x => x.Name) // if a role starts with __umbracoRole we won't show it as it's an internal role used for public access diff --git a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs index 233f053333..6be981b3d1 100644 --- a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs @@ -53,30 +53,7 @@ namespace Umbraco.Cms.Core.Security public override Task GetNormalizedUserNameAsync(MemberIdentityUser user, CancellationToken cancellationToken = default) => GetUserNameAsync(user, cancellationToken); /// - public override Task SetNormalizedUserNameAsync(MemberIdentityUser user, string normalizedName, - CancellationToken cancellationToken = default) - { - try - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - if (normalizedName == null) - { - throw new ArgumentNullException(nameof(normalizedName)); - } - - return SetUserNameAsync(user, normalizedName, cancellationToken); - } - catch (Exception ex) - { - return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = genericIdentityErrorCode, Description = ex.Message })); - } - } + public override Task SetNormalizedUserNameAsync(MemberIdentityUser user, string normalizedName, CancellationToken cancellationToken = default) => SetUserNameAsync(user, normalizedName, cancellationToken); /// public override Task CreateAsync(MemberIdentityUser user, CancellationToken cancellationToken = default) From dccce92cb33cb90e8ce915931daac630107e73d4 Mon Sep 17 00:00:00 2001 From: emmagarland Date: Sun, 14 Mar 2021 15:14:53 +0000 Subject: [PATCH 20/78] Updated the user and roles to throw exceptions instead of being caught, as per PR feedback --- .../Security/MemberRoleStore.cs | 143 ++++++++---------- .../Security/MemberRoleStoreTests.cs | 22 ++- ...rStoreTests.cs => MemberUserStoreTests.cs} | 14 +- 3 files changed, 84 insertions(+), 95 deletions(-) rename src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/{MemberIdentityUserStoreTests.cs => MemberUserStoreTests.cs} (92%) diff --git a/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs b/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs index d76f3b5c7b..8f4bd0cd17 100644 --- a/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs @@ -35,110 +35,89 @@ namespace Umbraco.Cms.Core.Security /// public Task CreateAsync(TRole role, CancellationToken cancellationToken = default) { - try + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (role == null) { - 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); + throw new ArgumentNullException(nameof(role)); } - catch (Exception ex) + + var memberGroup = new MemberGroup { - return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = genericIdentityErrorCode, Description = ex.Message })); - } + Name = role.Name + }; + + _memberGroupService.Save(memberGroup); + + role.Id = memberGroup.Id.ToString(); + + return Task.FromResult(IdentityResult.Success); } /// public Task UpdateAsync(TRole role, CancellationToken cancellationToken = default) { - try + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (role == null) { - 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); - } - //TODO: if nothing changed, do we need to report this? - return Task.FromResult(IdentityResult.Success); - } - else - { - //TODO: throw exception when not found, or return failure? - return Task.FromResult(IdentityResult.Failed(_memberGroupNotFoundError)); - } - + throw new ArgumentNullException(nameof(role)); } - catch (Exception ex) + + if (!int.TryParse(role.Id, out int roleId)) { - return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = ex.Message, Description = ex.Message })); + return Task.FromResult(IdentityResult.Failed(_intParseError)); } + + IMemberGroup memberGroup = _memberGroupService.GetById(roleId); + if (memberGroup != null) + { + if (MapToMemberGroup(role, memberGroup)) + { + _memberGroupService.Save(memberGroup); + } + //TODO: if nothing changed, do we need to report this? + return Task.FromResult(IdentityResult.Success); + } + else + { + //TODO: throw exception when not found, or return failure? + return Task.FromResult(IdentityResult.Failed(_memberGroupNotFoundError)); + } + } /// public Task DeleteAsync(TRole role, CancellationToken cancellationToken = default) { - try + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (role == null) { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - - if (!int.TryParse(role.Id, out int roleId)) - { - //TODO: what identity error should we return in this case? - return Task.FromResult(IdentityResult.Failed(_intParseError)); - } - - IMemberGroup memberGroup = _memberGroupService.GetById(roleId); - if (memberGroup != null) - { - _memberGroupService.Delete(memberGroup); - } - else - { - return Task.FromResult(IdentityResult.Failed(_memberGroupNotFoundError)); - } - - return Task.FromResult(IdentityResult.Success); + throw new ArgumentNullException(nameof(role)); } - catch (Exception ex) + + if (!int.TryParse(role.Id, out int roleId)) { - return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = ex.Message, Description = ex.Message })); + //TODO: what identity error should we return in this case? + return Task.FromResult(IdentityResult.Failed(_intParseError)); } + + IMemberGroup memberGroup = _memberGroupService.GetById(roleId); + if (memberGroup != null) + { + _memberGroupService.Delete(memberGroup); + } + else + { + return Task.FromResult(IdentityResult.Failed(_memberGroupNotFoundError)); + } + + return Task.FromResult(IdentityResult.Success); } /// diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs index 2c3cb901fc..a6c534d24d 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs @@ -33,14 +33,13 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security var fakeCancellationToken = new CancellationToken(); // act - Task actual = sut.CreateAsync(null, fakeCancellationToken); + Action actual = () => sut.CreateAsync(null, fakeCancellationToken); // assert - Assert.IsFalse(actual.Result.Succeeded); - Assert.IsTrue(actual.Result.Errors.Any(x => x.Code == "IdentityErrorUserStore" && x.Description == "Value cannot be null. (Parameter 'role')")); + Assert.That(actual, Throws.ArgumentNullException); + _mockMemberGroupService.VerifyNoOtherCalls(); } - [Test] public async Task GivenICreateAMemberRole_AndTheGroupIsPopulatedCorrectly_ThenIShouldGetASuccessResultAsync() { @@ -170,6 +169,21 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security _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() { diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs similarity index 92% rename from src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs index de8ba1e36b..7f80ff9382 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Data; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -17,7 +16,7 @@ using Umbraco.Cms.Tests.UnitTests.Umbraco.Core.ShortStringHelper; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security { [TestFixture] - public class MemberIdentityUserStoreTests + public class MemberUserStoreTests { private Mock _mockMemberService; @@ -70,17 +69,16 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security var fakeCancellationToken = new CancellationToken() { }; // act - var actual = (Task)sut.SetNormalizedUserNameAsync(null, "username", fakeCancellationToken); + Action actual = () => sut.SetNormalizedUserNameAsync(null, "username", fakeCancellationToken); // assert - Assert.IsFalse(actual.Result.Succeeded); - Assert.IsTrue(actual.Result.Errors.Any(x => x.Code == "IdentityErrorUserStore" && x.Description == "Value cannot be null. (Parameter 'user')")); + Assert.That(actual, Throws.ArgumentNullException); _mockMemberService.VerifyNoOtherCalls(); } [Test] - public void GivenISetNormalizedUserName_AndTheUserNameIsNull_ThenIShouldGetANull() + public void GivenISetNormalizedUserName_AndTheUserNameIsNull_ThenAnExceptionShouldBeThrown() { // arrange MemberUserStore sut = CreateSut(); @@ -88,11 +86,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security var fakeUser = new MemberIdentityUser() { }; // act - var actual = (Task)sut.SetNormalizedUserNameAsync(fakeUser, null, fakeCancellationToken); + Action actual = () => sut.SetNormalizedUserNameAsync(fakeUser, null, fakeCancellationToken); // assert - Assert.IsFalse(actual.Result.Succeeded); - Assert.IsTrue(actual.Result.Errors.Any(x => x.Code == "IdentityErrorUserStore" && x.Description == "Value cannot be null. (Parameter 'normalizedName')")); _mockMemberService.VerifyNoOtherCalls(); } From de390be2b8840631824f0b13bfeced75c1363b16 Mon Sep 17 00:00:00 2001 From: emmagarland Date: Sun, 14 Mar 2021 18:58:33 +0000 Subject: [PATCH 21/78] Added SignInManager for members and related unit tests (in progress) --- ...rManagerTests.cs => MemberManagerTests.cs} | 2 +- .../Security/MemberSignInManagerTests.cs | 110 +++++++++ src/Umbraco.Web.Common/Constants/Security.cs | 14 ++ .../Security/MemberManager.cs | 7 +- .../Security/MemberSignInManager.cs | 232 ++++++++++++++++++ 5 files changed, 358 insertions(+), 7 deletions(-) rename src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/{MemberIdentityUserManagerTests.cs => MemberManagerTests.cs} (99%) create mode 100644 src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs create mode 100644 src/Umbraco.Web.Common/Constants/Security.cs create mode 100644 src/Umbraco.Web.Common/Security/MemberSignInManager.cs diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs similarity index 99% rename from src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserManagerTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs index a319d33b34..4fe7ac5712 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserManagerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs @@ -17,7 +17,7 @@ using Umbraco.Cms.Web.Common.Security; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security { [TestFixture] - public class MemberIdentityUserManagerTests + public class MemberManagerTests { private Mock> _mockMemberStore; private Mock> _mockIdentityOptions; diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs new file mode 100644 index 0000000000..8dfc4a8a8a --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Net; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Web.Common.Security; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security +{ + [TestFixture] + public class MemberSignInManagerTests + { + 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> _mockPasswordConfiguration; + + public MemberSignInManager CreateSut() + { + _mockMemberStore = new Mock>(); + _mockIdentityOptions = new Mock>(); + + var idOptions = new MemberIdentityOptions { Lockout = { AllowedForNewUsers = false } }; + _mockIdentityOptions.Setup(o => o.Value).Returns(idOptions); + _mockPasswordHasher = new Mock>(); + + var userValidators = new List>(); + _mockUserValidators = new Mock>(); + var validator = new Mock>(); + userValidators.Add(validator.Object); + + _mockPasswordValidators = new Mock>>(); + _mockNormalizer = new Mock(); + _mockErrorDescriber = new IdentityErrorDescriber(); + _mockServiceProviders = new Mock(); + _mockLogger = new Mock>>(); + _mockPasswordConfiguration = new Mock>(); + _mockPasswordConfiguration.Setup(x => x.Value).Returns(() => + new MemberPasswordConfigurationSettings() + { + + }); + + var pwdValidators = new List> + { + new PasswordValidator() + }; + + var userManager = new MemberManager( + new Mock().Object, + _mockMemberStore.Object, + _mockIdentityOptions.Object, + _mockPasswordHasher.Object, + userValidators, + pwdValidators, + new BackOfficeIdentityErrorDescriber(), + _mockServiceProviders.Object, + new Mock().Object, + new Mock>>().Object, + _mockPasswordConfiguration.Object); + + validator.Setup(v => v.ValidateAsync( + userManager, + It.IsAny())) + .Returns(Task.FromResult(IdentityResult.Success)).Verifiable(); + + return new MemberSignInManager( + userManager, + Mock.Of(), + Mock.Of>(), + Mock.Of>(), + Mock.Of>>(), + Mock.Of(), + Mock.Of>()); + } + + [Test] + public async Task WhenPasswordSignInAsyncIsCalled_AndEverythingIsSetup_ThenASignInResultShouldBeReturnedAsync() + { + //arrange + MemberSignInManager sut = CreateSut(); + var fakeUser = new MemberIdentityUser(777) + { + }; + string password = null; + bool lockoutOnfailure = false; + bool isPersistent = false; + + //act + SignInResult actual = await sut.PasswordSignInAsync(fakeUser, password, isPersistent, lockoutOnfailure); + + //assert + Assert.IsTrue(actual.Succeeded); + } + } +} diff --git a/src/Umbraco.Web.Common/Constants/Security.cs b/src/Umbraco.Web.Common/Constants/Security.cs new file mode 100644 index 0000000000..f2fcbc65f8 --- /dev/null +++ b/src/Umbraco.Web.Common/Constants/Security.cs @@ -0,0 +1,14 @@ +namespace Umbraco.Cms.Web.Common.Constants +{ + public static class Security + { + //TODO: implement 2Factor external + + public const string MemberAuthenticationType = "UmbracoMember"; + //public const string MemberExternalAuthenticationType = "UmbracoMemberExternalCookie"; + //public const string MemberExternalCookieName = "UMB_MEMBEREXTLOGIN"; + public const string MemberTokenAuthenticationType = "UmbracoMemberToken"; + //public const string MemberTwoFactorAuthenticationType = "UmbracoMemberTwoFactorCookie"; + //public const string MemberTwoFactorRememberMeAuthenticationType = "UmbracoMemberTwoFactorRememberMeCookie"; + } +} diff --git a/src/Umbraco.Web.Common/Security/MemberManager.cs b/src/Umbraco.Web.Common/Security/MemberManager.cs index ae91852f46..7762df9bb7 100644 --- a/src/Umbraco.Web.Common/Security/MemberManager.cs +++ b/src/Umbraco.Web.Common/Security/MemberManager.cs @@ -32,11 +32,7 @@ namespace Umbraco.Cms.Web.Common.Security IHttpContextAccessor httpContextAccessor, ILogger> logger, IOptions passwordConfiguration) - : base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, errors, services, logger, passwordConfiguration) - { - _httpContextAccessor = httpContextAccessor; - } - + : base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, errors, services, logger, passwordConfiguration) => _httpContextAccessor = httpContextAccessor; private string GetCurrentUserId(IPrincipal currentUser) { ClaimsIdentity umbIdentity = currentUser?.GetUmbracoIdentity(); @@ -52,7 +48,6 @@ namespace Umbraco.Cms.Web.Common.Security } //TODO: have removed all other member audit events - can revisit if we need member auditing on a user level in future - public void RaiseForgotPasswordRequestedEvent(IPrincipal currentUser, string userId) => throw new NotImplementedException(); public void RaiseForgotPasswordChangedSuccessEvent(IPrincipal currentUser, string userId) => throw new NotImplementedException(); diff --git a/src/Umbraco.Web.Common/Security/MemberSignInManager.cs b/src/Umbraco.Web.Common/Security/MemberSignInManager.cs new file mode 100644 index 0000000000..09c7379a28 --- /dev/null +++ b/src/Umbraco.Web.Common/Security/MemberSignInManager.cs @@ -0,0 +1,232 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Security; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Web.Common.Security +{ + //TODO: can any of this be combined/merged with BackOfficeSignInManager using T for the identity user? + //TODO: Need to implement events on member login/logout etc + public class MemberSignInManager : SignInManager + { + private const string ClaimType = "amr"; + private const string PasswordValue = "pwd"; + + public MemberSignInManager( + MemberManager memberManager, + IHttpContextAccessor contextAccessor, + IUserClaimsPrincipalFactory claimsFactory, + IOptions optionsAccessor, + ILogger> logger, + IAuthenticationSchemeProvider schemes, + IUserConfirmation confirmation) : + base(memberManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) + { + } + + /// + public override async Task PasswordSignInAsync(MemberIdentityUser user, string password, bool isPersistent, bool lockoutOnFailure) + { + // overridden to handle logging/events + SignInResult result = await base.PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure); + return await HandleSignIn(user, user.UserName, result); + } + + /// + public override async Task PasswordSignInAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure) + { + // overridden to handle logging/events + MemberIdentityUser user = await UserManager.FindByNameAsync(userName); + if (user == null) + { + return await HandleSignIn(null, userName, SignInResult.Failed); + } + + return await PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure); + } + + /// + public override Task GetTwoFactorAuthenticationUserAsync() => throw new NotImplementedException("Two factor is not yet implemented for members"); + + /// + public override Task TwoFactorSignInAsync(string provider, string code, bool isPersistent, bool rememberClient) => throw new NotImplementedException("Two factor is not yet implemented for members"); + + /// + public override bool IsSignedIn(ClaimsPrincipal principal) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L126 + // replaced in order to use a custom auth type + // taken from BackOfficeSignInManager + + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + return principal?.Identities != null && principal.Identities.Any(i => i.AuthenticationType == Constants.Security.MemberAuthenticationType); + } + + /// + public override async Task RefreshSignInAsync(MemberIdentityUser user) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L126 + // replaced in order to use a custom auth type + + AuthenticateResult auth = await Context.AuthenticateAsync(Constants.Security.MemberAuthenticationType); + IList claims = Array.Empty(); + + Claim authenticationMethod = auth?.Principal?.FindFirst(ClaimTypes.AuthenticationMethod); + Claim amr = auth?.Principal?.FindFirst(ClaimType); + + if (authenticationMethod != null || amr != null) + { + claims = new List(); + if (authenticationMethod != null) + { + claims.Add(authenticationMethod); + } + if (amr != null) + { + claims.Add(amr); + } + } + + await SignInWithClaimsAsync(user, auth?.Properties, claims); + } + + /// + public override async Task SignInWithClaimsAsync(MemberIdentityUser user, AuthenticationProperties authenticationProperties, IEnumerable additionalClaims) + { + // TODO: taken from BackOfficeSigninManager and more notes are there + // override to replace IdentityConstants.ApplicationScheme with Constants.Security.MemberAuthenticationType + // code taken from aspnetcore: https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + // we also override to set the current HttpContext principal since this isn't done by default + + ClaimsPrincipal userPrincipal = await CreateUserPrincipalAsync(user); + foreach (Claim claim in additionalClaims) + { + userPrincipal.Identities.First().AddClaim(claim); + } + + // TODO: For future, this method gets called when performing 2FA logins + await Context.SignInAsync(Constants.Security.MemberAuthenticationType, + userPrincipal, + authenticationProperties ?? new AuthenticationProperties()); + } + + /// + public override async Task SignOutAsync() => + //TODO: does members need this custom signout type as per BackOfficeSignInManager? + // override to replace IdentityConstants.ApplicationScheme with Constants.Security.MemberAuthenticationType + // code taken from aspnetcore: https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + await Context.SignOutAsync(Constants.Security.MemberAuthenticationType); + + /// + public override Task IsTwoFactorClientRememberedAsync(MemberIdentityUser user) => throw new NotImplementedException("Two factor is not yet implemented for members"); + + /// + public override Task RememberTwoFactorClientAsync(MemberIdentityUser user) => throw new NotImplementedException("Two factor is not yet implemented for members"); + + /// + public override Task ForgetTwoFactorClientAsync() => throw new NotImplementedException("Two factor is not yet implemented for members"); + + /// + public override Task TwoFactorRecoveryCodeSignInAsync(string recoveryCode) => throw new NotImplementedException("Two factor is not yet implemented for members"); + + /// + public override Task GetExternalLoginInfoAsync(string expectedXsrf = null) => throw new NotImplementedException("External login is not yet implemented for members"); + + /// + public override AuthenticationProperties ConfigureExternalAuthenticationProperties(string provider, string redirectUrl, string userId = null) => throw new NotImplementedException("External login is not yet implemented for members"); + + /// + public override Task> GetExternalAuthenticationSchemesAsync() => throw new NotImplementedException("External login is not yet implemented for members"); + + /// + /// TODO: Two factor is not yet implemented for members + protected override async Task SignInOrTwoFactorAsync(MemberIdentityUser user, bool isPersistent, string loginProvider = null, bool bypassTwoFactor = false) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + // to replace custom auth types + + //TODO: There is currently no two factor so this needs changing once implemented + if (loginProvider != null) + { + await SignInAsync(user, isPersistent, loginProvider); + } + else + { + await SignInWithClaimsAsync(user, isPersistent, new Claim[] + { + new Claim(ClaimType, PasswordValue) + }); + } + return SignInResult.Success; + } + + /// + /// Called on any login attempt to update the AccessFailedCount and to raise events + /// + /// + /// + /// + /// + private async Task HandleSignIn(MemberIdentityUser user, string username, SignInResult result) + { + // TODO: More TODO notes in BackOfficeSignInManager + if (username.IsNullOrWhiteSpace()) + { + username = "UNKNOWN"; + } + + if (result.Succeeded) + { + //track the last login date + user.LastLoginDateUtc = DateTime.UtcNow; + if (user.AccessFailedCount > 0) + { + //we have successfully logged in, reset the AccessFailedCount + user.AccessFailedCount = 0; + } + await UserManager.UpdateAsync(user); + + Logger.LogInformation("User: {UserName} logged in from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress); + if (user != null) + { + //TODO: what events do we want for members? + //_memberManager.RaiseLoginSuccessEvent(Context.User, user.Id); + } + } + else if (result.IsLockedOut) + { + //TODO: what events do we want for members? + //_memberManager.RaiseAccountLockedEvent(Context.User, user.Id); + Logger.LogInformation("Login attempt failed for username {UserName} from IP address {IpAddress}, the user is locked", username, Context.Connection.RemoteIpAddress); + } + else if (result.RequiresTwoFactor) + { + //TODO: what events do we want for members? + //_memberManager.RaiseLoginRequiresVerificationEvent(Context.User, user.Id); + Logger.LogInformation("Login attempt requires verification for username {UserName} from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress); + } + else if (!result.Succeeded || result.IsNotAllowed) + { + Logger.LogInformation("Login attempt failed for username {UserName} from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress); + } + else + { + throw new ArgumentOutOfRangeException(); + } + + return result; + } + } +} From a260bc37a8fc571247bfd81b2761437c761299ce Mon Sep 17 00:00:00 2001 From: emmagarland Date: Sun, 14 Mar 2021 21:59:30 +0000 Subject: [PATCH 22/78] Fixed unit tests to use correct store, added IPResolver --- .../Security/MemberManagerTests.cs | 50 +++++++++------ .../Security/MemberSignInManagerTests.cs | 64 ++++++++++++++----- .../Security/MemberSignInManager.cs | 18 +++--- 3 files changed, 90 insertions(+), 42 deletions(-) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs index 4fe7ac5712..800b7923a4 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs @@ -10,8 +10,14 @@ using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Net; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; using Umbraco.Cms.Web.Common.Security; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security @@ -19,9 +25,10 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security [TestFixture] public class MemberManagerTests { - private Mock> _mockMemberStore; + private MemberUserStore _fakeMemberStore; private Mock> _mockIdentityOptions; private Mock> _mockPasswordHasher; + private Mock _mockMemberService; private Mock> _mockUserValidators; private Mock>> _mockPasswordValidators; private Mock _mockNormalizer; @@ -32,9 +39,14 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public MemberManager CreateSut() { - _mockMemberStore = new Mock>(); - _mockIdentityOptions = new Mock>(); + _mockMemberService = new Mock(); + _fakeMemberStore = new MemberUserStore( + _mockMemberService.Object, + new UmbracoMapper(new MapDefinitionCollection(new List())), + new Mock().Object, + new IdentityErrorDescriber()); + _mockIdentityOptions = new Mock>(); var idOptions = new MemberIdentityOptions { Lockout = { AllowedForNewUsers = false } }; _mockIdentityOptions.Setup(o => o.Value).Returns(idOptions); _mockPasswordHasher = new Mock>(); @@ -63,7 +75,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security var userManager = new MemberManager( new Mock().Object, - _mockMemberStore.Object, + _fakeMemberStore, _mockIdentityOptions.Object, _mockPasswordHasher.Object, userValidators, @@ -101,10 +113,6 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security } }; - _mockMemberStore.Setup(x => - x.CreateAsync(fakeUser, fakeCancellationToken)) - .ReturnsAsync(IdentityResult.Failed(identityErrors)); - //act IdentityResult identityResult = await sut.CreateAsync(fakeUser); @@ -130,10 +138,6 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security } }; - _mockMemberStore.Setup(x => - x.CreateAsync(null, fakeCancellationToken)) - .ReturnsAsync(IdentityResult.Failed(identityErrors)); - //act var identityResult = new Func>(() => sut.CreateAsync(null)); @@ -142,20 +146,30 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security Assert.That(identityResult, Throws.ArgumentNullException); } - [Test] public async Task GivenICreateANewUser_AndTheUserIsPopulatedCorrectly_ThenIShouldGetASuccessResultAsync() { //arrange MemberManager sut = CreateSut(); - MemberIdentityUser fakeUser = new MemberIdentityUser() + var fakeUser = new MemberIdentityUser(777) { + UserName = "testUser", + Email = "test@test.com", + Name = "Test", + MemberTypeAlias = "Anything", PasswordConfig = "testConfig" }; - CancellationToken fakeCancellationToken = new CancellationToken() { }; - _mockMemberStore.Setup(x => - x.CreateAsync(fakeUser, fakeCancellationToken)) - .ReturnsAsync(IdentityResult.Success); + + var builder = new MemberTypeBuilder(); + MemberType memberType = builder.BuildSimpleMemberType(); + + IMember fakeMember = new Member(memberType) + { + Id = 777 + }; + + _mockMemberService.Setup(x => x.CreateMember(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(fakeMember); + _mockMemberService.Setup(x => x.Save(fakeMember, false)); //act IdentityResult identityResult = await sut.CreateAsync(fakeUser); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs index 8dfc4a8a8a..26742eb1d8 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs @@ -9,8 +9,11 @@ using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Net; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.Security; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security @@ -18,7 +21,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security [TestFixture] public class MemberSignInManagerTests { - private Mock> _mockMemberStore; + private MemberUserStore _fakeMemberStore; private Mock> _mockIdentityOptions; private Mock> _mockPasswordHasher; private Mock> _mockUserValidators; @@ -26,12 +29,20 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security private Mock _mockNormalizer; private IdentityErrorDescriber _mockErrorDescriber; private Mock _mockServiceProviders; - private Mock>> _mockLogger; + private Mock> _mockLogger; private Mock> _mockPasswordConfiguration; + private MemberManager _userManager; + private Mock _mockIpResolver = new Mock(); public MemberSignInManager CreateSut() { - _mockMemberStore = new Mock>(); + var _mockMemberService = new Mock(); + _fakeMemberStore = new MemberUserStore( + _mockMemberService.Object, + new UmbracoMapper(new MapDefinitionCollection(new List())), + new Mock().Object, + new IdentityErrorDescriber()); + _mockIdentityOptions = new Mock>(); var idOptions = new MemberIdentityOptions { Lockout = { AllowedForNewUsers = false } }; @@ -47,22 +58,19 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security _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() - { - - }); + new MemberPasswordConfigurationSettings() { }); var pwdValidators = new List> { new PasswordValidator() }; - var userManager = new MemberManager( - new Mock().Object, - _mockMemberStore.Object, + _userManager = new MemberManager( + _mockIpResolver.Object, + _fakeMemberStore, _mockIdentityOptions.Object, _mockPasswordHasher.Object, userValidators, @@ -74,12 +82,13 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security _mockPasswordConfiguration.Object); validator.Setup(v => v.ValidateAsync( - userManager, + _userManager, It.IsAny())) .Returns(Task.FromResult(IdentityResult.Success)).Verifiable(); return new MemberSignInManager( - userManager, + _userManager, + Mock.Of(), Mock.Of(), Mock.Of>(), Mock.Of>(), @@ -89,16 +98,17 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security } [Test] - public async Task WhenPasswordSignInAsyncIsCalled_AndEverythingIsSetup_ThenASignInResultShouldBeReturnedAsync() + public async Task WhenPasswordSignInAsyncIsCalled_AndEverythingIsSetup_ThenASignInResultSucceededShouldBeReturnedAsync() { //arrange MemberSignInManager sut = CreateSut(); var fakeUser = new MemberIdentityUser(777) { + UserName = "TestUser", }; - string password = null; + string password = "testPassword"; bool lockoutOnfailure = false; - bool isPersistent = false; + bool isPersistent = true; //act SignInResult actual = await sut.PasswordSignInAsync(fakeUser, password, isPersistent, lockoutOnfailure); @@ -106,5 +116,27 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security //assert Assert.IsTrue(actual.Succeeded); } + + [Test] + public async Task WhenPasswordSignInAsyncIsCalled_AndTheResultFails_ThenASignInFailedResultShouldBeReturnedAsync() + { + //arrange + MemberSignInManager sut = CreateSut(); + var fakeUser = new MemberIdentityUser(777) + { + UserName = "TestUser", + }; + string password = "testPassword"; + bool lockoutOnfailure = false; + bool isPersistent = true; + _mockIpResolver.Setup(x => x.GetCurrentRequestIpAddress()).Returns("127.0.0.1"); + + //act + SignInResult actual = await sut.PasswordSignInAsync(fakeUser, password, isPersistent, lockoutOnfailure); + + //assert + Assert.IsFalse(actual.Succeeded); + //_mockLogger.Verify(x => x.LogInformation("Login attempt failed for username TestUser from IP address 127.0.0.1", null)); + } } } diff --git a/src/Umbraco.Web.Common/Security/MemberSignInManager.cs b/src/Umbraco.Web.Common/Security/MemberSignInManager.cs index 09c7379a28..38b23d8e28 100644 --- a/src/Umbraco.Web.Common/Security/MemberSignInManager.cs +++ b/src/Umbraco.Web.Common/Security/MemberSignInManager.cs @@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Net; using Umbraco.Cms.Core.Security; using Umbraco.Extensions; @@ -20,18 +20,19 @@ namespace Umbraco.Cms.Web.Common.Security { private const string ClaimType = "amr"; private const string PasswordValue = "pwd"; + private readonly IIpResolver _ipResolver; public MemberSignInManager( MemberManager memberManager, + IIpResolver ipResolver, IHttpContextAccessor contextAccessor, IUserClaimsPrincipalFactory claimsFactory, IOptions optionsAccessor, ILogger> logger, IAuthenticationSchemeProvider schemes, IUserConfirmation confirmation) : - base(memberManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) - { - } + base(memberManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) => + _ipResolver = ipResolver ?? throw new ArgumentNullException(nameof(ipResolver)); /// public override async Task PasswordSignInAsync(MemberIdentityUser user, string password, bool isPersistent, bool lockoutOnFailure) @@ -184,6 +185,7 @@ namespace Umbraco.Cms.Web.Common.Security // TODO: More TODO notes in BackOfficeSignInManager if (username.IsNullOrWhiteSpace()) { + //TODO: this might have unwanted effects if the member is called this username = "UNKNOWN"; } @@ -198,7 +200,7 @@ namespace Umbraco.Cms.Web.Common.Security } await UserManager.UpdateAsync(user); - Logger.LogInformation("User: {UserName} logged in from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress); + Logger.LogInformation("User: {UserName} logged in from IP address {IpAddress}", username, _ipResolver.GetCurrentRequestIpAddress()); if (user != null) { //TODO: what events do we want for members? @@ -209,17 +211,17 @@ namespace Umbraco.Cms.Web.Common.Security { //TODO: what events do we want for members? //_memberManager.RaiseAccountLockedEvent(Context.User, user.Id); - Logger.LogInformation("Login attempt failed for username {UserName} from IP address {IpAddress}, the user is locked", username, Context.Connection.RemoteIpAddress); + Logger.LogInformation("Login attempt failed for username {UserName} from IP address {IpAddress}, the user is locked", username, _ipResolver.GetCurrentRequestIpAddress()); } else if (result.RequiresTwoFactor) { //TODO: what events do we want for members? //_memberManager.RaiseLoginRequiresVerificationEvent(Context.User, user.Id); - Logger.LogInformation("Login attempt requires verification for username {UserName} from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress); + Logger.LogInformation("Login attempt requires verification for username {UserName} from IP address {IpAddress}", username, _ipResolver.GetCurrentRequestIpAddress()); } else if (!result.Succeeded || result.IsNotAllowed) { - Logger.LogInformation("Login attempt failed for username {UserName} from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress); + Logger.LogInformation("Login attempt failed for username {UserName} from IP address {IpAddress}", username, _ipResolver.GetCurrentRequestIpAddress()); } else { From 76e0f8474884573abfb709276fef4c62b9d96e0a Mon Sep 17 00:00:00 2001 From: emmagarland Date: Sun, 14 Mar 2021 22:19:27 +0000 Subject: [PATCH 23/78] Amendments to tests for mocking --- .../Security/MemberSignInManagerTests.cs | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs index 26742eb1d8..595dfb6ccd 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs @@ -31,8 +31,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security private Mock _mockServiceProviders; private Mock> _mockLogger; private Mock> _mockPasswordConfiguration; - private MemberManager _userManager; - private Mock _mockIpResolver = new Mock(); + private Mock _memberManager; + private readonly Mock _mockIpResolver = new Mock(); public MemberSignInManager CreateSut() { @@ -68,26 +68,27 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security new PasswordValidator() }; - _userManager = new MemberManager( - _mockIpResolver.Object, - _fakeMemberStore, - _mockIdentityOptions.Object, - _mockPasswordHasher.Object, - userValidators, - pwdValidators, - new BackOfficeIdentityErrorDescriber(), - _mockServiceProviders.Object, - new Mock().Object, - new Mock>>().Object, - _mockPasswordConfiguration.Object); + //_memberManager = new MemberManager( + // _mockIpResolver.Object, + // _fakeMemberStore, + // _mockIdentityOptions.Object, + // _mockPasswordHasher.Object, + // userValidators, + // pwdValidators, + // new BackOfficeIdentityErrorDescriber(), + // _mockServiceProviders.Object, + // new Mock().Object, + // new Mock>>().Object, + // _mockPasswordConfiguration.Object); - validator.Setup(v => v.ValidateAsync( - _userManager, - It.IsAny())) - .Returns(Task.FromResult(IdentityResult.Success)).Verifiable(); + //validator.Setup(v => v.ValidateAsync( + // _memberManager, + // It.IsAny())) + // .Returns(Task.FromResult(IdentityResult.Success)).Verifiable(); + _memberManager = new Mock(); return new MemberSignInManager( - _userManager, + _memberManager.As().Object, Mock.Of(), Mock.Of(), Mock.Of>(), @@ -109,6 +110,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security string password = "testPassword"; bool lockoutOnfailure = false; bool isPersistent = true; + _memberManager.Setup(x => x.FindByNameAsync(It.IsAny())).ReturnsAsync(fakeUser); //act SignInResult actual = await sut.PasswordSignInAsync(fakeUser, password, isPersistent, lockoutOnfailure); From ef178c560671c2a2da284d45798c6b8d88435e1a Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 16 Mar 2021 09:21:27 +0100 Subject: [PATCH 24/78] Use explicitly typed notifications instead of generic ones --- .../Cache/DistributedCacheHandler.cs | 32 +++++++++---------- .../Compose/AuditNotificationsHandler.cs | 20 ++++++------ .../Compose/NotificationsComposer.cs | 32 +++++++++---------- .../Events/UserNotificationsHandler.cs | 4 +-- .../FileUploadPropertyEditor.cs | 4 +-- .../ImageCropperPropertyEditor.cs | 4 +-- .../Services/Implement/MemberService.cs | 14 ++++---- .../Services/Implement/PublicAccessService.cs | 16 +++++----- .../Services/Implement/UserService.cs | 28 ++++++++-------- .../MemberDeletedNotification.cs | 15 +++++++++ .../MemberDeletingNotification.cs | 20 ++++++++++++ .../Notifications/MemberSavedNotification.cs | 20 ++++++++++++ .../Notifications/MemberSavingNotification.cs | 20 ++++++++++++ .../PublicAccessEntryDeletedNotification.cs | 15 +++++++++ .../PublicAccessEntryDeletingNotification.cs | 20 ++++++++++++ .../PublicAccessEntrySavedNotification.cs | 20 ++++++++++++ .../PublicAccessEntrySavingNotification.cs | 20 ++++++++++++ .../Notifications/UserDeletedNotification.cs | 15 +++++++++ .../Notifications/UserDeletingNotification.cs | 20 ++++++++++++ .../UserGroupDeletedNotification.cs | 15 +++++++++ .../UserGroupDeletingNotification.cs | 20 ++++++++++++ .../UserGroupSavedNotification.cs | 20 ++++++++++++ .../UserGroupSavingNotification.cs | 20 ++++++++++++ .../UserGroupWithUsersSavedNotification.cs | 19 +++++++++++ .../UserGroupWithUsersSavingNotification.cs | 19 +++++++++++ .../Notifications/UserSavedNotification.cs | 20 ++++++++++++ .../Notifications/UserSavingNotification.cs | 20 ++++++++++++ 27 files changed, 415 insertions(+), 77 deletions(-) create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/MemberDeletedNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/MemberDeletingNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/MemberSavedNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/MemberSavingNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/PublicAccessEntryDeletedNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/PublicAccessEntryDeletingNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/PublicAccessEntrySavedNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/PublicAccessEntrySavingNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/UserDeletedNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/UserDeletingNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/UserGroupDeletedNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/UserGroupDeletingNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/UserGroupSavedNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/UserGroupSavingNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/UserGroupWithUsersSavedNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/UserGroupWithUsersSavingNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/UserSavedNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/UserSavingNotification.cs diff --git a/src/Umbraco.Infrastructure/Cache/DistributedCacheHandler.cs b/src/Umbraco.Infrastructure/Cache/DistributedCacheHandler.cs index 648edaca68..793c78f68b 100644 --- a/src/Umbraco.Infrastructure/Cache/DistributedCacheHandler.cs +++ b/src/Umbraco.Infrastructure/Cache/DistributedCacheHandler.cs @@ -8,24 +8,24 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Core.Cache { internal class DistributedCacheHandler : - INotificationHandler>, - INotificationHandler>, - INotificationHandler>, - INotificationHandler>, - INotificationHandler>, - INotificationHandler>, - INotificationHandler>, - INotificationHandler> + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler { private readonly DistributedCache _distributedCache; public DistributedCacheHandler(DistributedCache distributedCache) => _distributedCache = distributedCache; - public void Handle(SavedNotification notification) => _distributedCache.RefreshMemberCache(notification.SavedEntities.ToArray()); + public void Handle(MemberSavedNotification notification) => _distributedCache.RefreshMemberCache(notification.SavedEntities.ToArray()); - public void Handle(DeletedNotification notification) => _distributedCache.RemoveMemberCache(notification.DeletedEntities.ToArray()); + public void Handle(MemberDeletedNotification notification) => _distributedCache.RemoveMemberCache(notification.DeletedEntities.ToArray()); - public void Handle(SavedNotification notification) + public void Handle(UserSavedNotification notification) { foreach (var entity in notification.SavedEntities) { @@ -33,7 +33,7 @@ namespace Umbraco.Cms.Core.Cache } } - public void Handle(DeletedNotification notification) + public void Handle(UserDeletedNotification notification) { foreach (var entity in notification.DeletedEntities) { @@ -41,7 +41,7 @@ namespace Umbraco.Cms.Core.Cache } } - public void Handle(SavedNotification notification) + public void Handle(UserGroupSavedNotification notification) { foreach (var entity in notification.SavedEntities) { @@ -49,7 +49,7 @@ namespace Umbraco.Cms.Core.Cache } } - public void Handle(DeletedNotification notification) + public void Handle(UserGroupDeletedNotification notification) { foreach (var entity in notification.DeletedEntities) { @@ -57,8 +57,8 @@ namespace Umbraco.Cms.Core.Cache } } - public void Handle(SavedNotification notification) => _distributedCache.RefreshPublicAccess(); + public void Handle(PublicAccessEntrySavedNotification notification) => _distributedCache.RefreshPublicAccess(); - public void Handle(DeletedNotification notification) => _distributedCache.RefreshPublicAccess(); + public void Handle(PublicAccessEntryDeletedNotification notification) => _distributedCache.RefreshPublicAccess(); } } diff --git a/src/Umbraco.Infrastructure/Compose/AuditNotificationsHandler.cs b/src/Umbraco.Infrastructure/Compose/AuditNotificationsHandler.cs index f152dd16cf..e2da324b56 100644 --- a/src/Umbraco.Infrastructure/Compose/AuditNotificationsHandler.cs +++ b/src/Umbraco.Infrastructure/Compose/AuditNotificationsHandler.cs @@ -15,14 +15,14 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Core.Compose { public sealed class AuditNotificationsHandler : - INotificationHandler>, - INotificationHandler>, + INotificationHandler, + INotificationHandler, INotificationHandler, INotificationHandler, INotificationHandler, - INotificationHandler>, - INotificationHandler>, - INotificationHandler>, + INotificationHandler, + INotificationHandler, + INotificationHandler, INotificationHandler { private readonly IAuditService _auditService; @@ -69,7 +69,7 @@ namespace Umbraco.Cms.Core.Compose private string FormatEmail(IUser user) => user == null ? string.Empty : user.Email.IsNullOrWhiteSpace() ? "" : $"<{user.Email}>"; - public void Handle(SavedNotification notification) + public void Handle(MemberSavedNotification notification) { var performingUser = CurrentPerformingUser; var members = notification.SavedEntities; @@ -84,7 +84,7 @@ namespace Umbraco.Cms.Core.Compose } } - public void Handle(DeletedNotification notification) + public void Handle(MemberDeletedNotification notification) { var performingUser = CurrentPerformingUser; var members = notification.DeletedEntities; @@ -138,7 +138,7 @@ namespace Umbraco.Cms.Core.Compose "umbraco/member/exported", "exported member data"); } - public void Handle(SavedNotification notification) + public void Handle(UserSavedNotification notification) { var performingUser = CurrentPerformingUser; var affectedUsers = notification.SavedEntities; @@ -157,7 +157,7 @@ namespace Umbraco.Cms.Core.Compose } } - public void Handle(DeletedNotification notification) + public void Handle(UserDeletedNotification notification) { var performingUser = CurrentPerformingUser; var affectedUsers = notification.DeletedEntities; @@ -168,7 +168,7 @@ namespace Umbraco.Cms.Core.Compose "umbraco/user/delete", "delete user"); } - public void Handle(SavedNotification notification) + public void Handle(UserGroupWithUsersSavedNotification notification) { var performingUser = CurrentPerformingUser; foreach (var groupWithUser in notification.SavedEntities) diff --git a/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs b/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs index e79cef4cb1..d973254659 100644 --- a/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs +++ b/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs @@ -31,7 +31,7 @@ namespace Umbraco.Cms.Core.Compose .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() - .AddNotificationHandler, UserNotificationsHandler>(); + .AddNotificationHandler(); // add handlers for building content relations builder @@ -51,12 +51,12 @@ namespace Umbraco.Cms.Core.Compose .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() - .AddNotificationHandler, FileUploadPropertyEditor>() + .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() - .AddNotificationHandler, ImageCropperPropertyEditor>(); + .AddNotificationHandler(); // add notification handlers for redirect tracking builder @@ -67,26 +67,26 @@ namespace Umbraco.Cms.Core.Compose // add notification handlers for auditing builder - .AddNotificationHandler, AuditNotificationsHandler>() - .AddNotificationHandler, AuditNotificationsHandler>() + .AddNotificationHandler() + .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() - .AddNotificationHandler, AuditNotificationsHandler>() - .AddNotificationHandler, AuditNotificationsHandler>() - .AddNotificationHandler, AuditNotificationsHandler>() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() .AddNotificationHandler(); // add notifications handlers for distributed cache builder - .AddNotificationHandler, DistributedCacheHandler>() - .AddNotificationHandler, DistributedCacheHandler>() - .AddNotificationHandler, DistributedCacheHandler>() - .AddNotificationHandler, DistributedCacheHandler>() - .AddNotificationHandler, DistributedCacheHandler>() - .AddNotificationHandler, DistributedCacheHandler>() - .AddNotificationHandler, DistributedCacheHandler>() - .AddNotificationHandler, DistributedCacheHandler>(); + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler(); } } } diff --git a/src/Umbraco.Infrastructure/Events/UserNotificationsHandler.cs b/src/Umbraco.Infrastructure/Events/UserNotificationsHandler.cs index 719692e38d..933a0fc890 100644 --- a/src/Umbraco.Infrastructure/Events/UserNotificationsHandler.cs +++ b/src/Umbraco.Infrastructure/Events/UserNotificationsHandler.cs @@ -31,7 +31,7 @@ namespace Umbraco.Cms.Core.Events INotificationHandler, INotificationHandler, INotificationHandler, - INotificationHandler> + INotificationHandler { private readonly Notifier _notifier; private readonly ActionCollection _actions; @@ -225,7 +225,7 @@ namespace Umbraco.Cms.Core.Events } - public void Handle(SavedNotification notification) + public void Handle(PublicAccessEntrySavedNotification notification) { var entities = _contentService.GetByIds(notification.SavedEntities.Select(e => e.ProtectedNodeId)).ToArray(); if (entities.Any() == false) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyEditor.cs index 33c31b6ee4..a794d62ce2 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyEditor.cs @@ -28,7 +28,7 @@ namespace Umbraco.Cms.Core.PropertyEditors public class FileUploadPropertyEditor : DataEditor, IMediaUrlGenerator, INotificationHandler, INotificationHandler, INotificationHandler, INotificationHandler, - INotificationHandler> + INotificationHandler { private readonly IMediaFileSystem _mediaFileSystem; private readonly ContentSettings _contentSettings; @@ -160,7 +160,7 @@ namespace Umbraco.Cms.Core.PropertyEditors public void Handle(MediaDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities); - public void Handle(DeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities); + public void Handle(MemberDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities); private void DeleteContainedFiles(IEnumerable deletedEntities) { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs index e96e8582a9..902cb32f08 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs @@ -35,7 +35,7 @@ namespace Umbraco.Cms.Core.PropertyEditors public class ImageCropperPropertyEditor : DataEditor, IMediaUrlGenerator, INotificationHandler, INotificationHandler, INotificationHandler, INotificationHandler, - INotificationHandler> + INotificationHandler { private readonly IMediaFileSystem _mediaFileSystem; private readonly ContentSettings _contentSettings; @@ -216,7 +216,7 @@ namespace Umbraco.Cms.Core.PropertyEditors public void Handle(MediaDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities); - public void Handle(DeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities); + public void Handle(MemberDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities); private void DeleteContainedFiles(IEnumerable deletedEntities) { diff --git a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs index 026052fa67..fd5c9480a7 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs @@ -777,7 +777,7 @@ namespace Umbraco.Cms.Core.Services.Implement using (IScope scope = ScopeProvider.CreateScope()) { - var savingNotification = new SavingNotification(member, evtMsgs); + var savingNotification = new MemberSavingNotification(member, evtMsgs); if (raiseEvents && scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); @@ -795,7 +795,7 @@ namespace Umbraco.Cms.Core.Services.Implement if (raiseEvents) { - scope.Notifications.Publish(new SavedNotification(member, evtMsgs).WithStateFrom(savingNotification)); + scope.Notifications.Publish(new MemberSavedNotification(member, evtMsgs).WithStateFrom(savingNotification)); } Audit(AuditType.Save, 0, member.Id); @@ -813,7 +813,7 @@ namespace Umbraco.Cms.Core.Services.Implement using (var scope = ScopeProvider.CreateScope()) { - var savingNotification = new SavingNotification(membersA, evtMsgs); + var savingNotification = new MemberSavingNotification(membersA, evtMsgs); if (raiseEvents && scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); @@ -833,7 +833,7 @@ namespace Umbraco.Cms.Core.Services.Implement if (raiseEvents) { - scope.Notifications.Publish(new SavedNotification(membersA, evtMsgs).WithStateFrom(savingNotification)); + scope.Notifications.Publish(new MemberSavedNotification(membersA, evtMsgs).WithStateFrom(savingNotification)); } Audit(AuditType.Save, 0, -1, "Save multiple Members"); @@ -855,7 +855,7 @@ namespace Umbraco.Cms.Core.Services.Implement using (var scope = ScopeProvider.CreateScope()) { - var deletingNotification = new DeletingNotification(member, evtMsgs); + var deletingNotification = new MemberDeletingNotification(member, evtMsgs); if (scope.Notifications.PublishCancelable(deletingNotification)) { scope.Complete(); @@ -874,7 +874,7 @@ namespace Umbraco.Cms.Core.Services.Implement { // a member has no descendants _memberRepository.Delete(member); - scope.Notifications.Publish(new DeletedNotification(member, evtMsgs).WithState(notificationState)); + scope.Notifications.Publish(new MemberDeletedNotification(member, evtMsgs).WithState(notificationState)); // media files deleted by QueuingEventDispatcher } @@ -1165,7 +1165,7 @@ namespace Umbraco.Cms.Core.Services.Implement IMember[] members = _memberRepository.Get(query).ToArray(); - if (scope.Notifications.PublishCancelable(new DeletingNotification(members, evtMsgs))) + if (scope.Notifications.PublishCancelable(new MemberDeletingNotification(members, evtMsgs))) { scope.Complete(); return; diff --git a/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs b/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs index 57df102620..b79b0ec0ab 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs @@ -128,7 +128,7 @@ namespace Umbraco.Cms.Core.Services.Implement return OperationResult.Attempt.Succeed(evtMsgs, entry); } - var savingNotifiation = new SavingNotification(entry, evtMsgs); + var savingNotifiation = new PublicAccessEntrySavingNotification(entry, evtMsgs); if (scope.Notifications.PublishCancelable(savingNotifiation)) { scope.Complete(); @@ -139,7 +139,7 @@ namespace Umbraco.Cms.Core.Services.Implement scope.Complete(); - scope.Notifications.Publish(new SavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); + scope.Notifications.Publish(new PublicAccessEntrySavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); } return OperationResult.Attempt.Succeed(evtMsgs, entry); @@ -165,7 +165,7 @@ namespace Umbraco.Cms.Core.Services.Implement entry.RemoveRule(existingRule); - var savingNotifiation = new SavingNotification(entry, evtMsgs); + var savingNotifiation = new PublicAccessEntrySavingNotification(entry, evtMsgs); if (scope.Notifications.PublishCancelable(savingNotifiation)) { scope.Complete(); @@ -175,7 +175,7 @@ namespace Umbraco.Cms.Core.Services.Implement _publicAccessRepository.Save(entry); scope.Complete(); - scope.Notifications.Publish(new SavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); + scope.Notifications.Publish(new PublicAccessEntrySavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); } return OperationResult.Attempt.Succeed(evtMsgs); @@ -191,7 +191,7 @@ namespace Umbraco.Cms.Core.Services.Implement using (var scope = ScopeProvider.CreateScope()) { - var savingNotifiation = new SavingNotification(entry, evtMsgs); + var savingNotifiation = new PublicAccessEntrySavingNotification(entry, evtMsgs); if (scope.Notifications.PublishCancelable(savingNotifiation)) { scope.Complete(); @@ -201,7 +201,7 @@ namespace Umbraco.Cms.Core.Services.Implement _publicAccessRepository.Save(entry); scope.Complete(); - scope.Notifications.Publish(new SavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); + scope.Notifications.Publish(new PublicAccessEntrySavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); } return OperationResult.Attempt.Succeed(evtMsgs); @@ -217,7 +217,7 @@ namespace Umbraco.Cms.Core.Services.Implement using (var scope = ScopeProvider.CreateScope()) { - var deletingNotification = new DeletingNotification(entry, evtMsgs); + var deletingNotification = new PublicAccessEntryDeletingNotification(entry, evtMsgs); if (scope.Notifications.PublishCancelable(deletingNotification)) { scope.Complete(); @@ -227,7 +227,7 @@ namespace Umbraco.Cms.Core.Services.Implement _publicAccessRepository.Delete(entry); scope.Complete(); - scope.Notifications.Publish(new DeletedNotification(entry, evtMsgs).WithStateFrom(deletingNotification)); + scope.Notifications.Publish(new PublicAccessEntryDeletedNotification(entry, evtMsgs).WithStateFrom(deletingNotification)); } return OperationResult.Attempt.Succeed(evtMsgs); diff --git a/src/Umbraco.Infrastructure/Services/Implement/UserService.cs b/src/Umbraco.Infrastructure/Services/Implement/UserService.cs index 3336ba37a3..4fbe579ca3 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/UserService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/UserService.cs @@ -132,7 +132,7 @@ namespace Umbraco.Cms.Core.Services.Implement IsApproved = isApproved }; - var savingNotification = new SavingNotification(user, evtMsgs); + var savingNotification = new UserSavingNotification(user, evtMsgs); if (scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); @@ -141,7 +141,7 @@ namespace Umbraco.Cms.Core.Services.Implement _userRepository.Save(user); - scope.Notifications.Publish(new SavedNotification(user, evtMsgs).WithStateFrom(savingNotification)); + scope.Notifications.Publish(new UserSavedNotification(user, evtMsgs).WithStateFrom(savingNotification)); scope.Complete(); } @@ -245,7 +245,7 @@ namespace Umbraco.Cms.Core.Services.Implement using (var scope = ScopeProvider.CreateScope()) { - var deletingNotification = new DeletingNotification(user, evtMsgs); + var deletingNotification = new UserDeletingNotification(user, evtMsgs); if (scope.Notifications.PublishCancelable(deletingNotification)) { scope.Complete(); @@ -254,7 +254,7 @@ namespace Umbraco.Cms.Core.Services.Implement _userRepository.Delete(user); - scope.Notifications.Publish(new DeletedNotification(user, evtMsgs).WithStateFrom(deletingNotification)); + scope.Notifications.Publish(new UserDeletedNotification(user, evtMsgs).WithStateFrom(deletingNotification)); scope.Complete(); } } @@ -279,7 +279,7 @@ namespace Umbraco.Cms.Core.Services.Implement using (var scope = ScopeProvider.CreateScope()) { - var savingNotification = new SavingNotification(entity, evtMsgs); + var savingNotification = new UserSavingNotification(entity, evtMsgs); if (raiseEvents && scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); @@ -297,7 +297,7 @@ namespace Umbraco.Cms.Core.Services.Implement _userRepository.Save(entity); if (raiseEvents) { - scope.Notifications.Publish(new SavedNotification(entity, evtMsgs).WithStateFrom(savingNotification)); + scope.Notifications.Publish(new UserSavedNotification(entity, evtMsgs).WithStateFrom(savingNotification)); } scope.Complete(); @@ -329,7 +329,7 @@ namespace Umbraco.Cms.Core.Services.Implement using (var scope = ScopeProvider.CreateScope()) { - var savingNotification = new SavingNotification(entitiesA, evtMsgs); + var savingNotification = new UserSavingNotification(entitiesA, evtMsgs); if (raiseEvents && scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); @@ -350,7 +350,7 @@ namespace Umbraco.Cms.Core.Services.Implement if (raiseEvents) { - scope.Notifications.Publish(new SavedNotification(entitiesA, evtMsgs).WithStateFrom(savingNotification)); + scope.Notifications.Publish(new UserSavedNotification(entitiesA, evtMsgs).WithStateFrom(savingNotification)); } //commit the whole lot in one go @@ -840,7 +840,7 @@ namespace Umbraco.Cms.Core.Services.Implement var userGroupWithUsers = new UserGroupWithUsers(userGroup, addedUsers, removedUsers); // this is the default/expected notification for the IUserGroup entity being saved - var savingNotification = new SavingNotification(userGroup, evtMsgs); + var savingNotification = new UserGroupSavingNotification(userGroup, evtMsgs); if (raiseEvents && scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); @@ -848,7 +848,7 @@ namespace Umbraco.Cms.Core.Services.Implement } // this is an additional notification for special auditing - var savingUserGroupWithUsersNotification = new SavingNotification(userGroupWithUsers, evtMsgs); + var savingUserGroupWithUsersNotification = new UserGroupWithUsersSavingNotification(userGroupWithUsers, evtMsgs); if (raiseEvents && scope.Notifications.PublishCancelable(savingUserGroupWithUsersNotification)) { scope.Complete(); @@ -859,8 +859,8 @@ namespace Umbraco.Cms.Core.Services.Implement if (raiseEvents) { - scope.Notifications.Publish(new SavedNotification(userGroup, evtMsgs).WithStateFrom(savingNotification)); - scope.Notifications.Publish(new SavedNotification(userGroupWithUsers, evtMsgs).WithStateFrom(savingUserGroupWithUsersNotification)); + scope.Notifications.Publish(new UserGroupSavedNotification(userGroup, evtMsgs).WithStateFrom(savingNotification)); + scope.Notifications.Publish(new UserGroupWithUsersSavedNotification(userGroupWithUsers, evtMsgs).WithStateFrom(savingUserGroupWithUsersNotification)); } scope.Complete(); @@ -877,7 +877,7 @@ namespace Umbraco.Cms.Core.Services.Implement using (var scope = ScopeProvider.CreateScope()) { - var deletingNotification = new DeletingNotification(userGroup, evtMsgs); + var deletingNotification = new UserGroupDeletingNotification(userGroup, evtMsgs); if (scope.Notifications.PublishCancelable(deletingNotification)) { scope.Complete(); @@ -886,7 +886,7 @@ namespace Umbraco.Cms.Core.Services.Implement _userGroupRepository.Delete(userGroup); - scope.Notifications.Publish(new DeletedNotification(userGroup, evtMsgs).WithStateFrom(deletingNotification)); + scope.Notifications.Publish(new UserGroupDeletedNotification(userGroup, evtMsgs).WithStateFrom(deletingNotification)); scope.Complete(); } diff --git a/src/Umbraco.Infrastructure/Services/Notifications/MemberDeletedNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/MemberDeletedNotification.cs new file mode 100644 index 0000000000..41edd5371e --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/MemberDeletedNotification.cs @@ -0,0 +1,15 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public sealed class MemberDeletedNotification : DeletedNotification + { + public MemberDeletedNotification(IMember target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/MemberDeletingNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/MemberDeletingNotification.cs new file mode 100644 index 0000000000..4a4cd585c5 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/MemberDeletingNotification.cs @@ -0,0 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public sealed class MemberDeletingNotification : DeletingNotification + { + public MemberDeletingNotification(IMember target, EventMessages messages) : base(target, messages) + { + } + + public MemberDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/MemberSavedNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/MemberSavedNotification.cs new file mode 100644 index 0000000000..6ea4e38870 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/MemberSavedNotification.cs @@ -0,0 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public sealed class MemberSavedNotification : SavedNotification + { + public MemberSavedNotification(IMember target, EventMessages messages) : base(target, messages) + { + } + + public MemberSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/MemberSavingNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/MemberSavingNotification.cs new file mode 100644 index 0000000000..8496731304 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/MemberSavingNotification.cs @@ -0,0 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public sealed class MemberSavingNotification : SavingNotification + { + public MemberSavingNotification(IMember target, EventMessages messages) : base(target, messages) + { + } + + public MemberSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/PublicAccessEntryDeletedNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/PublicAccessEntryDeletedNotification.cs new file mode 100644 index 0000000000..03f48dff5c --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/PublicAccessEntryDeletedNotification.cs @@ -0,0 +1,15 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public sealed class PublicAccessEntryDeletedNotification : DeletedNotification + { + public PublicAccessEntryDeletedNotification(PublicAccessEntry target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/PublicAccessEntryDeletingNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/PublicAccessEntryDeletingNotification.cs new file mode 100644 index 0000000000..521a86caff --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/PublicAccessEntryDeletingNotification.cs @@ -0,0 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public sealed class PublicAccessEntryDeletingNotification : DeletingNotification + { + public PublicAccessEntryDeletingNotification(PublicAccessEntry target, EventMessages messages) : base(target, messages) + { + } + + public PublicAccessEntryDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/PublicAccessEntrySavedNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/PublicAccessEntrySavedNotification.cs new file mode 100644 index 0000000000..ec1781a3d4 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/PublicAccessEntrySavedNotification.cs @@ -0,0 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public sealed class PublicAccessEntrySavedNotification : SavedNotification + { + public PublicAccessEntrySavedNotification(PublicAccessEntry target, EventMessages messages) : base(target, messages) + { + } + + public PublicAccessEntrySavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/PublicAccessEntrySavingNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/PublicAccessEntrySavingNotification.cs new file mode 100644 index 0000000000..63f4a490a3 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/PublicAccessEntrySavingNotification.cs @@ -0,0 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public sealed class PublicAccessEntrySavingNotification : SavingNotification + { + public PublicAccessEntrySavingNotification(PublicAccessEntry target, EventMessages messages) : base(target, messages) + { + } + + public PublicAccessEntrySavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/UserDeletedNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/UserDeletedNotification.cs new file mode 100644 index 0000000000..3173647652 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/UserDeletedNotification.cs @@ -0,0 +1,15 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models.Membership; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public sealed class UserDeletedNotification : DeletedNotification + { + public UserDeletedNotification(IUser target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/UserDeletingNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/UserDeletingNotification.cs new file mode 100644 index 0000000000..1f57755a5a --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/UserDeletingNotification.cs @@ -0,0 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models.Membership; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public sealed class UserDeletingNotification : DeletingNotification + { + public UserDeletingNotification(IUser target, EventMessages messages) : base(target, messages) + { + } + + public UserDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/UserGroupDeletedNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/UserGroupDeletedNotification.cs new file mode 100644 index 0000000000..a9c51dc72d --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/UserGroupDeletedNotification.cs @@ -0,0 +1,15 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models.Membership; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public sealed class UserGroupDeletedNotification : DeletedNotification + { + public UserGroupDeletedNotification(IUserGroup target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/UserGroupDeletingNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/UserGroupDeletingNotification.cs new file mode 100644 index 0000000000..c176e55456 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/UserGroupDeletingNotification.cs @@ -0,0 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models.Membership; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public sealed class UserGroupDeletingNotification : DeletingNotification + { + public UserGroupDeletingNotification(IUserGroup target, EventMessages messages) : base(target, messages) + { + } + + public UserGroupDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/UserGroupSavedNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/UserGroupSavedNotification.cs new file mode 100644 index 0000000000..b542b35b26 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/UserGroupSavedNotification.cs @@ -0,0 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models.Membership; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public sealed class UserGroupSavedNotification : SavedNotification + { + public UserGroupSavedNotification(IUserGroup target, EventMessages messages) : base(target, messages) + { + } + + public UserGroupSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/UserGroupSavingNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/UserGroupSavingNotification.cs new file mode 100644 index 0000000000..136b2da6a5 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/UserGroupSavingNotification.cs @@ -0,0 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models.Membership; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public sealed class UserGroupSavingNotification : SavingNotification + { + public UserGroupSavingNotification(IUserGroup target, EventMessages messages) : base(target, messages) + { + } + + public UserGroupSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/UserGroupWithUsersSavedNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/UserGroupWithUsersSavedNotification.cs new file mode 100644 index 0000000000..22e0d67d06 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/UserGroupWithUsersSavedNotification.cs @@ -0,0 +1,19 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public sealed class UserGroupWithUsersSavedNotification : SavedNotification + { + public UserGroupWithUsersSavedNotification(UserGroupWithUsers target, EventMessages messages) : base(target, messages) + { + } + + public UserGroupWithUsersSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/UserGroupWithUsersSavingNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/UserGroupWithUsersSavingNotification.cs new file mode 100644 index 0000000000..c61087038a --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/UserGroupWithUsersSavingNotification.cs @@ -0,0 +1,19 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public sealed class UserGroupWithUsersSavingNotification : SavingNotification + { + public UserGroupWithUsersSavingNotification(UserGroupWithUsers target, EventMessages messages) : base(target, messages) + { + } + + public UserGroupWithUsersSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/UserSavedNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/UserSavedNotification.cs new file mode 100644 index 0000000000..850085ae90 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/UserSavedNotification.cs @@ -0,0 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models.Membership; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public sealed class UserSavedNotification : SavedNotification + { + public UserSavedNotification(IUser target, EventMessages messages) : base(target, messages) + { + } + + public UserSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/UserSavingNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/UserSavingNotification.cs new file mode 100644 index 0000000000..0b062c33f1 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/UserSavingNotification.cs @@ -0,0 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models.Membership; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public sealed class UserSavingNotification : SavingNotification + { + public UserSavingNotification(IUser target, EventMessages messages) : base(target, messages) + { + } + + public UserSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} From 152dc7a5b9d6d80911e50cdb570249997d25d5d8 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 16 Mar 2021 09:59:44 +0100 Subject: [PATCH 25/78] A little reformatting and removal of unused usings --- src/Umbraco.Infrastructure/Cache/DistributedCacheHandler.cs | 2 -- src/Umbraco.Infrastructure/Events/UserNotificationsHandler.cs | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/Umbraco.Infrastructure/Cache/DistributedCacheHandler.cs b/src/Umbraco.Infrastructure/Cache/DistributedCacheHandler.cs index 793c78f68b..a72b37c189 100644 --- a/src/Umbraco.Infrastructure/Cache/DistributedCacheHandler.cs +++ b/src/Umbraco.Infrastructure/Cache/DistributedCacheHandler.cs @@ -1,7 +1,5 @@ using System.Linq; using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Infrastructure.Services.Notifications; using Umbraco.Extensions; diff --git a/src/Umbraco.Infrastructure/Events/UserNotificationsHandler.cs b/src/Umbraco.Infrastructure/Events/UserNotificationsHandler.cs index 933a0fc890..3df16dda25 100644 --- a/src/Umbraco.Infrastructure/Events/UserNotificationsHandler.cs +++ b/src/Umbraco.Infrastructure/Events/UserNotificationsHandler.cs @@ -211,7 +211,6 @@ namespace Umbraco.Cms.Core.Events })); } } - } public void Handle(AssignedUserGroupPermissionsNotification notification) @@ -222,7 +221,6 @@ namespace Umbraco.Cms.Core.Events return; } _notifier.Notify(_actions.GetAction(), entities); - } public void Handle(PublicAccessEntrySavedNotification notification) From 3018e1ba894da6b9523f5b3cb94a2c399b1207f3 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 16 Mar 2021 16:45:26 +0100 Subject: [PATCH 26/78] Removed events from integration test --- .../Cache/DistributedCacheBinderTests.cs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs b/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs index 9b8a1e9c98..8fbe858294 100644 --- a/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs +++ b/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs @@ -5,7 +5,6 @@ using NUnit.Framework; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; @@ -18,7 +17,6 @@ namespace Umbraco.Cms.Tests.Integration.Cache [UmbracoTest(Boot = true)] public class DistributedCacheBinderTests : UmbracoIntegrationTest { - private IUserService UserService => GetRequiredService(); private ILocalizationService LocalizationService => GetRequiredService(); private IDataTypeService DataTypeService => GetRequiredService(); private IFileService FileService => GetRequiredService(); @@ -27,11 +25,7 @@ namespace Umbraco.Cms.Tests.Integration.Cache private IDomainService DomainService => GetRequiredService(); private IMemberTypeService MemberTypeService => GetRequiredService(); private IMacroService MacroService => GetRequiredService(); - private IMemberService MemberService => GetRequiredService(); private IMemberGroupService MemberGroupService => GetRequiredService(); - private IMediaService MediaService => GetRequiredService(); - private IContentService ContentService => GetRequiredService(); - private IPublicAccessService PublicAccessService => GetRequiredService(); private IRelationService RelationService => GetRequiredService(); private UriUtility UriUtility => GetRequiredService(); private IUmbracoContextFactory UmbracoContextFactory => GetRequiredService(); @@ -42,11 +36,6 @@ namespace Umbraco.Cms.Tests.Integration.Cache var definitions = new IEventDefinition[] { - new EventDefinition>(null, UserService, new SaveEventArgs(Enumerable.Empty())), - new EventDefinition>(null, UserService, new DeleteEventArgs(Enumerable.Empty())), - new EventDefinition>(null, UserService, new SaveEventArgs(Enumerable.Empty())), - new EventDefinition>(null, UserService, new DeleteEventArgs(Enumerable.Empty())), - new EventDefinition>(null, LocalizationService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, LocalizationService, new DeleteEventArgs(Enumerable.Empty())), @@ -76,9 +65,6 @@ namespace Umbraco.Cms.Tests.Integration.Cache new EventDefinition>(null, MacroService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, MacroService, new DeleteEventArgs(Enumerable.Empty())), - new EventDefinition>(null, MemberService, new SaveEventArgs(Enumerable.Empty())), - new EventDefinition>(null, MemberService, new DeleteEventArgs(Enumerable.Empty())), - new EventDefinition>(null, MemberGroupService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, MemberGroupService, new DeleteEventArgs(Enumerable.Empty())), @@ -86,9 +72,6 @@ namespace Umbraco.Cms.Tests.Integration.Cache //new EventDefinition>(null, ContentService, new SaveEventArgs(Enumerable.Empty()), "SavedBlueprint"), //new EventDefinition>(null, ContentService, new DeleteEventArgs(Enumerable.Empty()), "DeletedBlueprint"), - new EventDefinition>(null, PublicAccessService, new SaveEventArgs(Enumerable.Empty())), - new EventDefinition>(null, PublicAccessService, new DeleteEventArgs(Enumerable.Empty())), - new EventDefinition>(null, RelationService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, RelationService, new DeleteEventArgs(Enumerable.Empty())), From 7a5af55cac938e28ea2ff7c7ad970f7c241bbbd3 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 17 Mar 2021 12:47:16 +0100 Subject: [PATCH 27/78] Reintroduce and obsolete static events for use in the distributed cache + added some accidentally removed scope completions --- .../Cache/DistributedCacheBinder_Handlers.cs | 77 +++++++++++++++++++ .../Cache/DistributedCacheHandler.cs | 62 --------------- .../Compose/NotificationsComposer.cs | 11 --- .../Services/Implement/MemberService.cs | 16 +++- .../Services/Implement/PublicAccessService.cs | 10 +++ .../Services/Implement/UserService.cs | 18 +++++ .../Cache/DistributedCacheBinderTests.cs | 17 ++++ 7 files changed, 134 insertions(+), 77 deletions(-) delete mode 100644 src/Umbraco.Infrastructure/Cache/DistributedCacheHandler.cs diff --git a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs index ae6b6f135b..d6fff0f99d 100644 --- a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs +++ b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs @@ -49,6 +49,16 @@ namespace Umbraco.Cms.Core.Cache _logger.LogInformation("Initializing Umbraco internal event handlers for cache refreshing."); + // bind to user and user group events + Bind(() => UserService.SavedUserGroup += UserService_SavedUserGroup, + () => UserService.SavedUserGroup -= UserService_SavedUserGroup); + Bind(() => UserService.DeletedUserGroup += UserService_DeletedUserGroup, + () => UserService.DeletedUserGroup -= UserService_DeletedUserGroup); + Bind(() => UserService.SavedUser += UserService_SavedUser, + () => UserService.SavedUser -= UserService_SavedUser); + Bind(() => UserService.DeletedUser += UserService_DeletedUser, + () => UserService.DeletedUser -= UserService_DeletedUser); + // bind to dictionary events Bind(() => LocalizationService.DeletedDictionaryItem += LocalizationService_DeletedDictionaryItem, () => LocalizationService.DeletedDictionaryItem -= LocalizationService_DeletedDictionaryItem); @@ -100,6 +110,10 @@ namespace Umbraco.Cms.Core.Cache () => MacroService.Deleted -= MacroService_Deleted); // bind to member events + Bind(() => MemberService.Saved += MemberService_Saved, + () => MemberService.Saved -= MemberService_Saved); + Bind(() => MemberService.Deleted += MemberService_Deleted, + () => MemberService.Deleted -= MemberService_Deleted); Bind(() => MemberGroupService.Saved += MemberGroupService_Saved, () => MemberGroupService.Saved -= MemberGroupService_Saved); Bind(() => MemberGroupService.Deleted += MemberGroupService_Deleted, @@ -119,6 +133,12 @@ namespace Umbraco.Cms.Core.Cache //Bind(() => ContentService.DeletedBlueprint += ContentService_DeletedBlueprint, // () => ContentService.DeletedBlueprint -= ContentService_DeletedBlueprint); + // bind to public access events + Bind(() => PublicAccessService.Saved += PublicAccessService_Saved, + () => PublicAccessService.Saved -= PublicAccessService_Saved); + Bind(() => PublicAccessService.Deleted += PublicAccessService_Deleted, + () => PublicAccessService.Deleted -= PublicAccessService_Deleted); + // bind to relation type events Bind(() => RelationService.SavedRelationType += RelationService_SavedRelationType, () => RelationService.SavedRelationType -= RelationService_SavedRelationType); @@ -126,6 +146,20 @@ namespace Umbraco.Cms.Core.Cache () => RelationService.DeletedRelationType -= RelationService_DeletedRelationType); } + #region PublicAccessService + + private void PublicAccessService_Saved(IPublicAccessService sender, SaveEventArgs e) + { + _distributedCache.RefreshPublicAccess(); + } + + private void PublicAccessService_Deleted(IPublicAccessService sender, DeleteEventArgs e) + { + _distributedCache.RefreshPublicAccess(); + } + + #endregion + #region ContentService /// @@ -259,6 +293,35 @@ namespace Umbraco.Cms.Core.Cache #endregion + #region UserService + + private void UserService_SavedUser(IUserService sender, SaveEventArgs e) + { + foreach (var entity in e.SavedEntities) + _distributedCache.RefreshUserCache(entity.Id); + } + + private void UserService_DeletedUser(IUserService sender, DeleteEventArgs e) + { + foreach (var entity in e.DeletedEntities) + _distributedCache.RemoveUserCache(entity.Id); + } + + private void UserService_SavedUserGroup(IUserService sender, SaveEventArgs e) + { + foreach (var entity in e.SavedEntities) + _distributedCache.RefreshUserGroupCache(entity.UserGroup.Id); + } + + private void UserService_DeletedUserGroup(IUserService sender, DeleteEventArgs e) + { + + foreach (var entity in e.DeletedEntities) + _distributedCache.RemoveUserGroupCache(entity.Id); + } + + #endregion + #region FileService /// @@ -314,6 +377,20 @@ namespace Umbraco.Cms.Core.Cache #endregion + #region MemberService + + private void MemberService_Deleted(IMemberService sender, DeleteEventArgs e) + { + _distributedCache.RemoveMemberCache(e.DeletedEntities.ToArray()); + } + + private void MemberService_Saved(IMemberService sender, SaveEventArgs e) + { + _distributedCache.RefreshMemberCache(e.SavedEntities.ToArray()); + } + + #endregion + #region MemberGroupService private void MemberGroupService_Deleted(IMemberGroupService sender, DeleteEventArgs e) diff --git a/src/Umbraco.Infrastructure/Cache/DistributedCacheHandler.cs b/src/Umbraco.Infrastructure/Cache/DistributedCacheHandler.cs deleted file mode 100644 index a72b37c189..0000000000 --- a/src/Umbraco.Infrastructure/Cache/DistributedCacheHandler.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Linq; -using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Infrastructure.Services.Notifications; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Core.Cache -{ - internal class DistributedCacheHandler : - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler - { - private readonly DistributedCache _distributedCache; - - public DistributedCacheHandler(DistributedCache distributedCache) => _distributedCache = distributedCache; - - public void Handle(MemberSavedNotification notification) => _distributedCache.RefreshMemberCache(notification.SavedEntities.ToArray()); - - public void Handle(MemberDeletedNotification notification) => _distributedCache.RemoveMemberCache(notification.DeletedEntities.ToArray()); - - public void Handle(UserSavedNotification notification) - { - foreach (var entity in notification.SavedEntities) - { - _distributedCache.RefreshUserCache(entity.Id); - } - } - - public void Handle(UserDeletedNotification notification) - { - foreach (var entity in notification.DeletedEntities) - { - _distributedCache.RemoveUserCache(entity.Id); - } - } - - public void Handle(UserGroupSavedNotification notification) - { - foreach (var entity in notification.SavedEntities) - { - _distributedCache.RefreshUserGroupCache(entity.Id); - } - } - - public void Handle(UserGroupDeletedNotification notification) - { - foreach (var entity in notification.DeletedEntities) - { - _distributedCache.RemoveUserGroupCache(entity.Id); - } - } - - public void Handle(PublicAccessEntrySavedNotification notification) => _distributedCache.RefreshPublicAccess(); - - public void Handle(PublicAccessEntryDeletedNotification notification) => _distributedCache.RefreshPublicAccess(); - } -} diff --git a/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs b/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs index d973254659..2e5c587f64 100644 --- a/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs +++ b/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs @@ -76,17 +76,6 @@ namespace Umbraco.Cms.Core.Compose .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler(); - - // add notifications handlers for distributed cache - builder - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler(); } } } diff --git a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs index fd5c9480a7..32fc6955cf 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs @@ -796,6 +796,7 @@ namespace Umbraco.Cms.Core.Services.Implement if (raiseEvents) { scope.Notifications.Publish(new MemberSavedNotification(member, evtMsgs).WithStateFrom(savingNotification)); + scope.Events.Dispatch(Saved, this, new SaveEventArgs(member, false)); } Audit(AuditType.Save, 0, member.Id); @@ -834,6 +835,7 @@ namespace Umbraco.Cms.Core.Services.Implement if (raiseEvents) { scope.Notifications.Publish(new MemberSavedNotification(membersA, evtMsgs).WithStateFrom(savingNotification)); + scope.Events.Dispatch(Saved, this, new SaveEventArgs(membersA, false)); } Audit(AuditType.Save, 0, -1, "Save multiple Members"); @@ -875,6 +877,7 @@ namespace Umbraco.Cms.Core.Services.Implement // a member has no descendants _memberRepository.Delete(member); scope.Notifications.Publish(new MemberDeletedNotification(member, evtMsgs).WithState(notificationState)); + scope.Events.Dispatch(Deleted, this, new DeleteEventArgs(member, false)); // media files deleted by QueuingEventDispatcher } @@ -1018,6 +1021,7 @@ namespace Umbraco.Cms.Core.Services.Implement int[] ids = _memberGroupRepository.GetMemberIds(usernames); _memberGroupRepository.AssignRoles(ids, roleNames); scope.Notifications.Publish(new AssignedMemberRolesNotification(ids, roleNames)); + scope.Complete(); } } @@ -1031,6 +1035,7 @@ namespace Umbraco.Cms.Core.Services.Implement int[] ids = _memberGroupRepository.GetMemberIds(usernames); _memberGroupRepository.DissociateRoles(ids, roleNames); scope.Notifications.Publish(new RemovedMemberRolesNotification(ids, roleNames)); + scope.Complete(); } } @@ -1043,6 +1048,7 @@ namespace Umbraco.Cms.Core.Services.Implement scope.WriteLock(Constants.Locks.MemberTree); _memberGroupRepository.AssignRoles(memberIds, roleNames); scope.Notifications.Publish(new AssignedMemberRolesNotification(memberIds, roleNames)); + scope.Complete(); } } @@ -1055,6 +1061,7 @@ namespace Umbraco.Cms.Core.Services.Implement scope.WriteLock(Constants.Locks.MemberTree); _memberGroupRepository.DissociateRoles(memberIds, roleNames); scope.Notifications.Publish(new RemovedMemberRolesNotification(memberIds, roleNames)); + scope.Complete(); } } @@ -1068,10 +1075,11 @@ namespace Umbraco.Cms.Core.Services.Implement #region Event Handlers - /// - /// Occurs after members have been exported. - /// - public static event TypedEventHandler Exported; + [Obsolete("Will be removed in an upcoming version. Implement an INotificationHandler for MemberDeletedNotification instead.")] + public static event TypedEventHandler> Deleted; + + [Obsolete("Will be removed in an upcoming version. Implement an INotificationHandler for MemberSavedNotification instead.")] + public static event TypedEventHandler> Saved; #endregion diff --git a/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs b/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs index b79b0ec0ab..afe309e8b4 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs @@ -140,6 +140,7 @@ namespace Umbraco.Cms.Core.Services.Implement scope.Complete(); scope.Notifications.Publish(new PublicAccessEntrySavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); + scope.Events.Dispatch(Saved, this, new SaveEventArgs(entry, false)); } return OperationResult.Attempt.Succeed(evtMsgs, entry); @@ -176,6 +177,7 @@ namespace Umbraco.Cms.Core.Services.Implement scope.Complete(); scope.Notifications.Publish(new PublicAccessEntrySavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); + scope.Events.Dispatch(Saved, this, new SaveEventArgs(entry, false)); } return OperationResult.Attempt.Succeed(evtMsgs); @@ -202,6 +204,7 @@ namespace Umbraco.Cms.Core.Services.Implement scope.Complete(); scope.Notifications.Publish(new PublicAccessEntrySavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); + scope.Events.Dispatch(Saved, this, new SaveEventArgs(entry, false)); } return OperationResult.Attempt.Succeed(evtMsgs); @@ -228,9 +231,16 @@ namespace Umbraco.Cms.Core.Services.Implement scope.Complete(); scope.Notifications.Publish(new PublicAccessEntryDeletedNotification(entry, evtMsgs).WithStateFrom(deletingNotification)); + scope.Events.Dispatch(Deleted, this, new DeleteEventArgs(entry, false)); } return OperationResult.Attempt.Succeed(evtMsgs); } + + [Obsolete("Will be removed in an upcoming version. Implement an INotificationHandler for PublicAccessEntrySavedNotification instead.")] + public static event TypedEventHandler> Saved; + + [Obsolete("Will be removed in an upcoming version. Implement an INotificationHandler for PublicAccessEntryDeletedNotification instead.")] + public static event TypedEventHandler> Deleted; } } diff --git a/src/Umbraco.Infrastructure/Services/Implement/UserService.cs b/src/Umbraco.Infrastructure/Services/Implement/UserService.cs index 4fbe579ca3..07694ffb1a 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/UserService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/UserService.cs @@ -142,6 +142,7 @@ namespace Umbraco.Cms.Core.Services.Implement _userRepository.Save(user); scope.Notifications.Publish(new UserSavedNotification(user, evtMsgs).WithStateFrom(savingNotification)); + scope.Events.Dispatch(SavedUser, this, new SaveEventArgs(user, false)); scope.Complete(); } @@ -255,6 +256,7 @@ namespace Umbraco.Cms.Core.Services.Implement _userRepository.Delete(user); scope.Notifications.Publish(new UserDeletedNotification(user, evtMsgs).WithStateFrom(deletingNotification)); + scope.Events.Dispatch(DeletedUser, this, new DeleteEventArgs(user, false)); scope.Complete(); } } @@ -298,6 +300,7 @@ namespace Umbraco.Cms.Core.Services.Implement if (raiseEvents) { scope.Notifications.Publish(new UserSavedNotification(entity, evtMsgs).WithStateFrom(savingNotification)); + scope.Events.Dispatch(SavedUser, this, new SaveEventArgs(entity, false)); } scope.Complete(); @@ -351,6 +354,7 @@ namespace Umbraco.Cms.Core.Services.Implement if (raiseEvents) { scope.Notifications.Publish(new UserSavedNotification(entitiesA, evtMsgs).WithStateFrom(savingNotification)); + scope.Events.Dispatch(SavedUser, this, new SaveEventArgs(entitiesA, false)); } //commit the whole lot in one go @@ -861,6 +865,7 @@ namespace Umbraco.Cms.Core.Services.Implement { scope.Notifications.Publish(new UserGroupSavedNotification(userGroup, evtMsgs).WithStateFrom(savingNotification)); scope.Notifications.Publish(new UserGroupWithUsersSavedNotification(userGroupWithUsers, evtMsgs).WithStateFrom(savingUserGroupWithUsersNotification)); + scope.Events.Dispatch(SavedUserGroup, this, new SaveEventArgs(new UserGroupWithUsers(userGroup, addedUsers, removedUsers), false)); } scope.Complete(); @@ -887,6 +892,7 @@ namespace Umbraco.Cms.Core.Services.Implement _userGroupRepository.Delete(userGroup); scope.Notifications.Publish(new UserGroupDeletedNotification(userGroup, evtMsgs).WithStateFrom(deletingNotification)); + scope.Events.Dispatch(DeletedUserGroup, this, new DeleteEventArgs(userGroup, false)); scope.Complete(); } @@ -1166,5 +1172,17 @@ namespace Umbraco.Cms.Core.Services.Implement } #endregion + + [Obsolete("Will be removed in an upcoming version. Implement an INotificationHandler for UserSavedNotification instead.")] + public static event TypedEventHandler> SavedUser; + + [Obsolete("Will be removed in an upcoming version. Implement an INotificationHandler for UserDeletedNotification instead.")] + public static event TypedEventHandler> DeletedUser; + + [Obsolete("Will be removed in an upcoming version. Implement an INotificationHandler for UserGroupSavedNotification instead.")] + public static event TypedEventHandler> SavedUserGroup; + + [Obsolete("Will be removed in an upcoming version. Implement an INotificationHandler for UserGroupDeletedNotification instead.")] + public static event TypedEventHandler> DeletedUserGroup; } } diff --git a/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs b/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs index 8fbe858294..9b8a1e9c98 100644 --- a/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs +++ b/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; @@ -17,6 +18,7 @@ namespace Umbraco.Cms.Tests.Integration.Cache [UmbracoTest(Boot = true)] public class DistributedCacheBinderTests : UmbracoIntegrationTest { + private IUserService UserService => GetRequiredService(); private ILocalizationService LocalizationService => GetRequiredService(); private IDataTypeService DataTypeService => GetRequiredService(); private IFileService FileService => GetRequiredService(); @@ -25,7 +27,11 @@ namespace Umbraco.Cms.Tests.Integration.Cache private IDomainService DomainService => GetRequiredService(); private IMemberTypeService MemberTypeService => GetRequiredService(); private IMacroService MacroService => GetRequiredService(); + private IMemberService MemberService => GetRequiredService(); private IMemberGroupService MemberGroupService => GetRequiredService(); + private IMediaService MediaService => GetRequiredService(); + private IContentService ContentService => GetRequiredService(); + private IPublicAccessService PublicAccessService => GetRequiredService(); private IRelationService RelationService => GetRequiredService(); private UriUtility UriUtility => GetRequiredService(); private IUmbracoContextFactory UmbracoContextFactory => GetRequiredService(); @@ -36,6 +42,11 @@ namespace Umbraco.Cms.Tests.Integration.Cache var definitions = new IEventDefinition[] { + new EventDefinition>(null, UserService, new SaveEventArgs(Enumerable.Empty())), + new EventDefinition>(null, UserService, new DeleteEventArgs(Enumerable.Empty())), + new EventDefinition>(null, UserService, new SaveEventArgs(Enumerable.Empty())), + new EventDefinition>(null, UserService, new DeleteEventArgs(Enumerable.Empty())), + new EventDefinition>(null, LocalizationService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, LocalizationService, new DeleteEventArgs(Enumerable.Empty())), @@ -65,6 +76,9 @@ namespace Umbraco.Cms.Tests.Integration.Cache new EventDefinition>(null, MacroService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, MacroService, new DeleteEventArgs(Enumerable.Empty())), + new EventDefinition>(null, MemberService, new SaveEventArgs(Enumerable.Empty())), + new EventDefinition>(null, MemberService, new DeleteEventArgs(Enumerable.Empty())), + new EventDefinition>(null, MemberGroupService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, MemberGroupService, new DeleteEventArgs(Enumerable.Empty())), @@ -72,6 +86,9 @@ namespace Umbraco.Cms.Tests.Integration.Cache //new EventDefinition>(null, ContentService, new SaveEventArgs(Enumerable.Empty()), "SavedBlueprint"), //new EventDefinition>(null, ContentService, new DeleteEventArgs(Enumerable.Empty()), "DeletedBlueprint"), + new EventDefinition>(null, PublicAccessService, new SaveEventArgs(Enumerable.Empty())), + new EventDefinition>(null, PublicAccessService, new DeleteEventArgs(Enumerable.Empty())), + new EventDefinition>(null, RelationService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, RelationService, new DeleteEventArgs(Enumerable.Empty())), From ab16a408a8b852287952f8d655243b665988f1b8 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Wed, 17 Mar 2021 14:04:48 +0100 Subject: [PATCH 28/78] Adds a config for configuring the access rules on the content dashboard - by default it granted for all user groups --- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 1 + .../config/content.dashboard.access.config.js | 22 ++++++++++++++++ .../Dashboards/ContentDashboard.cs | 26 ++++++++++++++----- 3 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 src/Umbraco.Web.UI/config/content.dashboard.access.config.js diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index eb6649a7c4..7fb27deb7e 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -148,6 +148,7 @@ True Settings.settings + diff --git a/src/Umbraco.Web.UI/config/content.dashboard.access.config.js b/src/Umbraco.Web.UI/config/content.dashboard.access.config.js new file mode 100644 index 0000000000..93c727d85a --- /dev/null +++ b/src/Umbraco.Web.UI/config/content.dashboard.access.config.js @@ -0,0 +1,22 @@ +[ + { + "Type": "grant", + "Value": "admin" + }, + { + "Type": "grant", + "Value": "editor" + }, + { + "Type": "grant", + "Value": "sensitiveData" + }, + { + "Type": "grant", + "Value": "translator" + }, + { + "Type": "grant", + "Value": "writer" + } +] diff --git a/src/Umbraco.Web/Dashboards/ContentDashboard.cs b/src/Umbraco.Web/Dashboards/ContentDashboard.cs index 0cd96f738c..3dbc0cb693 100644 --- a/src/Umbraco.Web/Dashboards/ContentDashboard.cs +++ b/src/Umbraco.Web/Dashboards/ContentDashboard.cs @@ -1,6 +1,9 @@ -using Umbraco.Core; +using System.IO; +using Newtonsoft.Json; +using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Dashboards; +using Umbraco.Core.IO; namespace Umbraco.Web.Dashboards { @@ -9,7 +12,7 @@ namespace Umbraco.Web.Dashboards { public string Alias => "contentIntro"; - public string[] Sections => new [] { "content" }; + public string[] Sections => new[] { "content" }; public string View => "views/dashboard/default/startupdashboardintro.html"; @@ -17,11 +20,22 @@ namespace Umbraco.Web.Dashboards { get { - var rules = new IAccessRule[] + IAccessRule[] rules; + var dashboardConfig = Path.Combine(IOHelper.MapPath(SystemDirectories.Config), "content.dashboard.access.config.js"); + + if (File.Exists(dashboardConfig)) { - new AccessRule {Type = AccessRuleType.Deny, Value = Constants.Security.TranslatorGroupAlias}, - new AccessRule {Type = AccessRuleType.Grant, Value = Constants.Security.AdminGroupAlias} - }; + var rawJson = File.ReadAllText(dashboardConfig); + rules = JsonConvert.DeserializeObject(rawJson); + } + else + { + rules = new IAccessRule[] + { + new AccessRule {Type = AccessRuleType.Deny, Value = Constants.Security.TranslatorGroupAlias}, + new AccessRule {Type = AccessRuleType.Grant, Value = Constants.Security.AdminGroupAlias} + }; + } return rules; } } From 3393ac8d0369c097e306d419201fb299c939a560 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Wed, 17 Mar 2021 14:05:47 +0100 Subject: [PATCH 29/78] Adds additional params indicating whether user is admin --- src/Umbraco.Web/Editors/DashboardController.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Editors/DashboardController.cs b/src/Umbraco.Web/Editors/DashboardController.cs index eef0b5df93..c7ce4e1220 100644 --- a/src/Umbraco.Web/Editors/DashboardController.cs +++ b/src/Umbraco.Web/Editors/DashboardController.cs @@ -17,6 +17,7 @@ using Umbraco.Core.Logging; using Umbraco.Core.Persistence; using Umbraco.Core.Services; using Umbraco.Core.Dashboards; +using Umbraco.Core.Models; using Umbraco.Web.Services; namespace Umbraco.Web.Editors @@ -52,8 +53,9 @@ namespace Umbraco.Web.Editors var allowedSections = string.Join(",", user.AllowedSections); var language = user.Language; var version = UmbracoVersion.SemanticVersion.ToSemanticString(); + var isAdmin = user.IsAdmin(); - var url = string.Format(baseUrl + "{0}?section={0}&allowed={1}&lang={2}&version={3}", section, allowedSections, language, version); + var url = string.Format(baseUrl + "{0}?section={0}&allowed={1}&lang={2}&version={3}&admin={4}", section, allowedSections, language, version, isAdmin); var key = "umbraco-dynamic-dashboard-" + language + allowedSections.Replace(",", "-") + section; var content = AppCaches.RuntimeCache.GetCacheItem(key); From e7a1db5604d4dd750a7093cfbdfa0b223ad026f9 Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Wed, 17 Mar 2021 21:54:38 +1000 Subject: [PATCH 30/78] Add images in grid - fixes 9982 (#9987) Co-authored-by: Sebastiaan Janssen (cherry picked from commit e2019777fbfc1f9221d040cb9f0b82c57f8552b9) --- .../grid/editors/media.controller.js | 40 ++++++++----------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js index 716ca405c1..94ea4b8604 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js @@ -1,9 +1,9 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.Grid.MediaController", - function ($scope, userService, editorService, localizationService) { - - $scope.thumbnailUrl = getThumbnailUrl(); - + function ($scope, userService, editorService, localizationService) { + + $scope.thumbnailUrl = getThumbnailUrl(); + if (!$scope.model.config.startNodeId) { if ($scope.model.config.ignoreUserStartNodes === true) { $scope.model.config.startNodeId = -1; @@ -29,16 +29,16 @@ angular.module("umbraco") onlyImages: true, dataTypeKey: $scope.model.dataTypeKey, submit: model => { - updateControlValue(model.selection[0]); + updateControlValue(model.selection[0]); editorService.close(); }, - close: () => editorService.close() + close: () => editorService.close() }; editorService.mediaPicker(mediaPicker); }; - $scope.editImage = function() { + $scope.editImage = function() { const mediaCropDetailsConfig = { size: 'small', @@ -47,17 +47,17 @@ angular.module("umbraco") updateControlValue(model.target); editorService.close(); }, - close: () => editorService.close() + close: () => editorService.close() }; localizationService.localize('defaultdialogs_editSelectedMedia').then(value => { mediaCropDetailsConfig.title = value; editorService.mediaCropDetails(mediaCropDetailsConfig); - }); + }); } - + /** - * + * */ function getThumbnailUrl() { @@ -94,19 +94,15 @@ angular.module("umbraco") return url; } - + return null; } /** - * - * @param {object} selectedImage + * + * @param {object} selectedImage */ function updateControlValue(selectedImage) { - - const doGetThumbnail = $scope.control.value.focalPoint !== selectedImage.focalPoint - || $scope.control.value.image !== selectedImage.image; - // we could apply selectedImage directly to $scope.control.value, // but this allows excluding fields in future if needed $scope.control.value = { @@ -118,10 +114,6 @@ angular.module("umbraco") caption: selectedImage.caption, altText: selectedImage.altText }; - - - if (doGetThumbnail) { - $scope.thumbnailUrl = getThumbnailUrl(); - } - } + $scope.thumbnailUrl = getThumbnailUrl(); + } }); From 548435dcc3f1f23df7f83dcc77fdfc87963ea355 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Wed, 17 Mar 2021 17:40:47 +0100 Subject: [PATCH 31/78] Bump version to 8.12.2 --- src/SolutionInfo.cs | 4 ++-- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index e92eefdf52..4162f47da6 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -18,5 +18,5 @@ using System.Resources; [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.12.1")] -[assembly: AssemblyInformationalVersion("8.12.1")] +[assembly: AssemblyFileVersion("8.12.2")] +[assembly: AssemblyInformationalVersion("8.12.2")] diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index eb6649a7c4..2ad23e1b73 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -347,9 +347,9 @@ False True - 8121 + 8122 / - http://localhost:8121 + http://localhost:8122 False False From e7e5e18b19844efacc8c456623e2d97051383a25 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Wed, 17 Mar 2021 17:44:49 +0000 Subject: [PATCH 32/78] Fixed merge issues --- src/Umbraco.Web.Common/Security/MemberManager.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.Common/Security/MemberManager.cs b/src/Umbraco.Web.Common/Security/MemberManager.cs index d491a010e4..195312a41e 100644 --- a/src/Umbraco.Web.Common/Security/MemberManager.cs +++ b/src/Umbraco.Web.Common/Security/MemberManager.cs @@ -12,7 +12,6 @@ namespace Umbraco.Cms.Web.Common.Security { public class MemberManager : UmbracoUserManager, IMemberManager { - public MemberManager( IIpResolver ipResolver, IUserStore store, @@ -24,7 +23,9 @@ namespace Umbraco.Cms.Web.Common.Security IServiceProvider services, ILogger> logger, IOptions passwordConfiguration) - : base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, errors, services, logger, passwordConfiguration) => _httpContextAccessor = httpContextAccessor; + : base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, errors, + services, logger, passwordConfiguration) + { } } } From 72f83e50d8fb99fbeee30bb43562c97b23e20bfe Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Wed, 17 Mar 2021 18:04:54 +0000 Subject: [PATCH 33/78] Changed param for UserManager --- .../Security/MemberSignInManagerTests.cs | 93 ++++--------------- .../Security/MemberSignInManager.cs | 2 +- 2 files changed, 19 insertions(+), 76 deletions(-) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs index 595dfb6ccd..a756c60380 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; @@ -8,12 +6,8 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Net; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Security; -using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.Security; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security @@ -21,74 +15,15 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security [TestFixture] public class MemberSignInManagerTests { - private MemberUserStore _fakeMemberStore; - 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> _mockPasswordConfiguration; - private Mock _memberManager; + private readonly Mock> _memberManager = MockUserManager(); private readonly Mock _mockIpResolver = new Mock(); public MemberSignInManager CreateSut() { - var _mockMemberService = new Mock(); - _fakeMemberStore = new MemberUserStore( - _mockMemberService.Object, - new UmbracoMapper(new MapDefinitionCollection(new List())), - new Mock().Object, - new IdentityErrorDescriber()); - - _mockIdentityOptions = new Mock>(); - - var idOptions = new MemberIdentityOptions { Lockout = { AllowedForNewUsers = false } }; - _mockIdentityOptions.Setup(o => o.Value).Returns(idOptions); - _mockPasswordHasher = new Mock>(); - - var userValidators = new List>(); - _mockUserValidators = new Mock>(); - var validator = new Mock>(); - userValidators.Add(validator.Object); - - _mockPasswordValidators = new Mock>>(); - _mockNormalizer = new Mock(); - _mockErrorDescriber = new IdentityErrorDescriber(); - _mockServiceProviders = new Mock(); _mockLogger = new Mock>(); - _mockPasswordConfiguration = new Mock>(); - _mockPasswordConfiguration.Setup(x => x.Value).Returns(() => - new MemberPasswordConfigurationSettings() { }); - - var pwdValidators = new List> - { - new PasswordValidator() - }; - - //_memberManager = new MemberManager( - // _mockIpResolver.Object, - // _fakeMemberStore, - // _mockIdentityOptions.Object, - // _mockPasswordHasher.Object, - // userValidators, - // pwdValidators, - // new BackOfficeIdentityErrorDescriber(), - // _mockServiceProviders.Object, - // new Mock().Object, - // new Mock>>().Object, - // _mockPasswordConfiguration.Object); - - //validator.Setup(v => v.ValidateAsync( - // _memberManager, - // It.IsAny())) - // .Returns(Task.FromResult(IdentityResult.Success)).Verifiable(); - - _memberManager = new Mock(); return new MemberSignInManager( - _memberManager.As().Object, + _memberManager.Object, Mock.Of(), Mock.Of(), Mock.Of>(), @@ -97,23 +32,31 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security Mock.Of(), Mock.Of>()); } + private static Mock> MockUserManager() where TUser : class + { + var store = new Mock>(); + var mgr = new Mock>(store.Object, null, null, null, null, null, null, null, null); + return mgr; + } [Test] public async Task WhenPasswordSignInAsyncIsCalled_AndEverythingIsSetup_ThenASignInResultSucceededShouldBeReturnedAsync() { //arrange + var userId = "bo8w3d32q9b98"; + _memberManager.Setup(x => x.GetUserIdAsync(It.IsAny())).ReturnsAsync(userId); MemberSignInManager sut = CreateSut(); var fakeUser = new MemberIdentityUser(777) { UserName = "TestUser", }; - string password = "testPassword"; - bool lockoutOnfailure = false; - bool isPersistent = true; + var password = "testPassword"; + var lockoutOnFailure = false; + var isPersistent = true; _memberManager.Setup(x => x.FindByNameAsync(It.IsAny())).ReturnsAsync(fakeUser); //act - SignInResult actual = await sut.PasswordSignInAsync(fakeUser, password, isPersistent, lockoutOnfailure); + SignInResult actual = await sut.PasswordSignInAsync(fakeUser, password, isPersistent, lockoutOnFailure); //assert Assert.IsTrue(actual.Succeeded); @@ -128,13 +71,13 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security { UserName = "TestUser", }; - string password = "testPassword"; - bool lockoutOnfailure = false; - bool isPersistent = true; + var password = "testPassword"; + var lockoutOnFailure = false; + var isPersistent = true; _mockIpResolver.Setup(x => x.GetCurrentRequestIpAddress()).Returns("127.0.0.1"); //act - SignInResult actual = await sut.PasswordSignInAsync(fakeUser, password, isPersistent, lockoutOnfailure); + SignInResult actual = await sut.PasswordSignInAsync(fakeUser, password, isPersistent, lockoutOnFailure); //assert Assert.IsFalse(actual.Succeeded); diff --git a/src/Umbraco.Web.Common/Security/MemberSignInManager.cs b/src/Umbraco.Web.Common/Security/MemberSignInManager.cs index 38b23d8e28..d3adcc4833 100644 --- a/src/Umbraco.Web.Common/Security/MemberSignInManager.cs +++ b/src/Umbraco.Web.Common/Security/MemberSignInManager.cs @@ -23,7 +23,7 @@ namespace Umbraco.Cms.Web.Common.Security private readonly IIpResolver _ipResolver; public MemberSignInManager( - MemberManager memberManager, + UserManager memberManager, IIpResolver ipResolver, IHttpContextAccessor contextAccessor, IUserClaimsPrincipalFactory claimsFactory, From 88820d082b7657109a9aa33b2fc0d4db03597394 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Wed, 17 Mar 2021 18:28:56 +0000 Subject: [PATCH 34/78] Setup for mocking --- .../Umbraco.Web.Common/Security/MemberSignInManagerTests.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs index a756c60380..49dc5d9d55 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs @@ -54,6 +54,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security var lockoutOnFailure = false; var isPersistent = true; _memberManager.Setup(x => x.FindByNameAsync(It.IsAny())).ReturnsAsync(fakeUser); + _memberManager.Setup(x => x.CheckPasswordAsync(fakeUser, password)).ReturnsAsync(true); + _memberManager.Setup(x => x.IsEmailConfirmedAsync(fakeUser)).ReturnsAsync(true); + _memberManager.Setup(x => x.IsLockedOutAsync(fakeUser)).ReturnsAsync(false); //act SignInResult actual = await sut.PasswordSignInAsync(fakeUser, password, isPersistent, lockoutOnFailure); From ed931a96294cf7fc48fb1c57bb30391707f8222f Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 18 Mar 2021 08:26:28 +0100 Subject: [PATCH 35/78] Fix build --- src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj | 6 ------ src/Umbraco.Web.Common/Umbraco.Web.Common.csproj | 2 -- 2 files changed, 8 deletions(-) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj index 1a7788591d..1bd1600721 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj @@ -37,10 +37,4 @@ - - - ..\Umbraco.Web\bin\Debug\Umbraco.Infrastructure.dll - - - diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index 738a9389dc..dce3ad7336 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -24,8 +24,6 @@ - - From 7193486b77333a14a94899ec5b35b7925f584e71 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 18 Mar 2021 08:38:57 +0100 Subject: [PATCH 36/78] Fix unit tests --- .../Controllers/MemberControllerUnitTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs index 8c8b6b0504..857f51dc20 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 @@ -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( @@ -389,7 +389,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers .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); From e17bf386a48df8fffa9e57f5360675e661f8eaac Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 18 Mar 2021 09:28:58 +0100 Subject: [PATCH 37/78] Merge pull request #9994 from umbraco/v8/bugfix/9993 Fixes #9993 - Cannot save empty image in Grid (cherry picked from commit 0ecc933921f2dea9a2a16d6f395b44a039663ec6) --- src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs index 862837381a..f9eacd9e73 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs @@ -202,8 +202,8 @@ namespace Umbraco.Web.PropertyEditors _richTextPropertyValueEditor.GetReferences(x.Value))) yield return umbracoEntityReference; - foreach (var umbracoEntityReference in mediaValues.SelectMany(x => - _mediaPickerPropertyValueEditor.GetReferences(x.Value["udi"]))) + foreach (var umbracoEntityReference in mediaValues.Where(x => x.Value.HasValues) + .SelectMany(x => _mediaPickerPropertyValueEditor.GetReferences(x.Value["udi"]))) yield return umbracoEntityReference; } } From 73439d4cbab197fa9292761c04c50b08db8663ae Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 18 Mar 2021 13:01:46 +0100 Subject: [PATCH 38/78] Fixes #9983 - Getting kicked, if document type has a Umbraco.UserPicker property (#10002) * Fixes #9983 Temporary fix for this issue. using the entityservice like before. * Needed to remove the call to usersResource here as well for displaying the picked items * Don't need usersResource for now (cherry picked from commit 45de0a101eaa2b8f16e21a765f32928c7cb968be) --- .../userpicker/userpicker.controller.js | 16 +++------- .../userpicker/userpicker.controller.js | 29 +++++++++++-------- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/userpicker/userpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/userpicker/userpicker.controller.js index a7021b2867..33d526c3cf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/userpicker/userpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/userpicker/userpicker.controller.js @@ -1,8 +1,8 @@ (function () { "use strict"; - function UserPickerController($scope, usersResource, localizationService, eventsService) { - + function UserPickerController($scope, entityResource, localizationService, eventsService) { + var vm = this; vm.users = []; @@ -102,17 +102,9 @@ vm.loading = true; // Get users - usersResource.getPagedResults(vm.usersOptions).then(function (users) { - - vm.users = users.items; - - vm.usersOptions.pageNumber = users.pageNumber; - vm.usersOptions.pageSize = users.pageSize; - vm.usersOptions.totalItems = users.totalItems; - vm.usersOptions.totalPages = users.totalPages; - + entityResource.getAll("User").then(function (data) { + vm.users = data; preSelect($scope.model.selection, vm.users); - vm.loading = false; }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/userpicker/userpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/userpicker/userpicker.controller.js index f2055fea3a..217a9c8421 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/userpicker/userpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/userpicker/userpicker.controller.js @@ -1,4 +1,4 @@ -function userPickerController($scope, usersResource , iconHelper, editorService, overlayService){ +function userPickerController($scope, iconHelper, editorService, overlayService, entityResource) { function trim(str, chr) { var rgxtrim = (!chr) ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^' + chr + '+|' + chr + '+$', 'g'); @@ -92,17 +92,22 @@ function userPickerController($scope, usersResource , iconHelper, editorService, unsubscribe(); }); - //load user data - var modelIds = $scope.model.value ? $scope.model.value.split(',') : []; - - // entityResource.getByIds doesn't support "User" and we would like to show avatars in umb-user-preview as well. - usersResource.getUsers(modelIds).then(function (data) { - _.each(data, function (item, i) { - // set default icon if it's missing - item.icon = item.icon ? iconHelper.convertFromLegacyIcon(item.icon) : "icon-user"; - $scope.renderModel.push({ name: item.name, id: item.id, udi: item.udi, icon: item.icon, avatars: item.avatars }); - }); - }); + //load user data - split to an array of ints (map) + const modelIds = $scope.model.value ? $scope.model.value.split(',').map(x => +x) : []; + if(modelIds.length !== 0) { + entityResource.getAll("User").then(function (users) { + const filteredUsers = users.filter(user => modelIds.indexOf(user.id) !== -1); + filteredUsers.forEach(item => { + $scope.renderModel.push({ + name: item.name, + id: item.id, + udi: item.udi, + icon: item.icon = item.icon ? iconHelper.convertFromLegacyIcon(item.icon) : "icon-user", + avatars: item.avatars + }); + }); + }); + } } From 7a699c3fa974016f670b7856ea11abbbefdf98b1 Mon Sep 17 00:00:00 2001 From: Mole Date: Wed, 24 Mar 2021 14:29:09 +0100 Subject: [PATCH 39/78] Add notification classes --- .../DictionaryItemDeletedNotification.cs | 15 ++++++++++++++ .../DictionaryItemDeletingNotification.cs | 20 +++++++++++++++++++ .../DictionaryItemSavedNotification.cs | 20 +++++++++++++++++++ .../DictionaryItemSavingNotification.cs | 20 +++++++++++++++++++ .../LanguageDeletedNotification.cs | 15 ++++++++++++++ .../LanguageDeletingNotification.cs | 20 +++++++++++++++++++ .../LanguageSavedNotification.cs | 20 +++++++++++++++++++ .../LanguageSavingNotification.cs | 20 +++++++++++++++++++ 8 files changed, 150 insertions(+) create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemDeletedNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemDeletingNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemSavedNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemSavingNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/LanguageDeletedNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/LanguageDeletingNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/LanguageSavedNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/LanguageSavingNotification.cs diff --git a/src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemDeletedNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemDeletedNotification.cs new file mode 100644 index 0000000000..3d3b9588d0 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemDeletedNotification.cs @@ -0,0 +1,15 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class DictionaryItemDeletedNotification : DeletedNotification + { + public DictionaryItemDeletedNotification(IDictionaryItem target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemDeletingNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemDeletingNotification.cs new file mode 100644 index 0000000000..ff85e00a81 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemDeletingNotification.cs @@ -0,0 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class DictionaryItemDeletingNotification : DeletingNotification + { + public DictionaryItemDeletingNotification(IDictionaryItem target, EventMessages messages) : base(target, messages) + { + } + + public DictionaryItemDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemSavedNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemSavedNotification.cs new file mode 100644 index 0000000000..3a64e35979 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemSavedNotification.cs @@ -0,0 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class DictionaryItemSavedNotification : SavedNotification + { + public DictionaryItemSavedNotification(IDictionaryItem target, EventMessages messages) : base(target, messages) + { + } + + public DictionaryItemSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemSavingNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemSavingNotification.cs new file mode 100644 index 0000000000..5f3d94697e --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemSavingNotification.cs @@ -0,0 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class DictionaryItemSavingNotification : SavingNotification + { + public DictionaryItemSavingNotification(IDictionaryItem target, EventMessages messages) : base(target, messages) + { + } + + public DictionaryItemSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/LanguageDeletedNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/LanguageDeletedNotification.cs new file mode 100644 index 0000000000..cba1c59406 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/LanguageDeletedNotification.cs @@ -0,0 +1,15 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class LanguageDeletedNotification : DeletedNotification + { + public LanguageDeletedNotification(ILanguage target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/LanguageDeletingNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/LanguageDeletingNotification.cs new file mode 100644 index 0000000000..79215e25af --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/LanguageDeletingNotification.cs @@ -0,0 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class LanguageDeletingNotification : DeletingNotification + { + public LanguageDeletingNotification(ILanguage target, EventMessages messages) : base(target, messages) + { + } + + public LanguageDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/LanguageSavedNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/LanguageSavedNotification.cs new file mode 100644 index 0000000000..87c3644df9 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/LanguageSavedNotification.cs @@ -0,0 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class LanguageSavedNotification : SavedNotification + { + public LanguageSavedNotification(ILanguage target, EventMessages messages) : base(target, messages) + { + } + + public LanguageSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/LanguageSavingNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/LanguageSavingNotification.cs new file mode 100644 index 0000000000..db416799d9 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/LanguageSavingNotification.cs @@ -0,0 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class LanguageSavingNotification : SavingNotification + { + public LanguageSavingNotification(ILanguage target, EventMessages messages) : base(target, messages) + { + } + + public LanguageSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} From eabfa7f414fcf89efbe34cf49b7e8563c0d2de3c Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Wed, 24 Mar 2021 15:28:06 +0100 Subject: [PATCH 40/78] Getting rid of the config file and implementing an appSetting instead --- .../Configuration/GlobalSettings.cs | 22 +++++++++++++++++++ .../Configuration/IGlobalSettings.cs | 9 ++++++++ src/Umbraco.Core/Constants-AppSettings.cs | 5 +++++ src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 1 - .../config/content.dashboard.access.config.js | 22 ------------------- src/Umbraco.Web.UI/web.Template.config | 1 + 6 files changed, 37 insertions(+), 23 deletions(-) delete mode 100644 src/Umbraco.Web.UI/config/content.dashboard.access.config.js diff --git a/src/Umbraco.Core/Configuration/GlobalSettings.cs b/src/Umbraco.Core/Configuration/GlobalSettings.cs index ba5baf9e87..0765a35a29 100644 --- a/src/Umbraco.Core/Configuration/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/GlobalSettings.cs @@ -395,6 +395,28 @@ namespace Umbraco.Core.Configuration } } + /// + /// Gets a value indicating whether the content dashboard should be available to all users. + /// + /// + /// true if the dashboard is visible for all user groups; otherwise, false + /// and the default access rules for that dashboard will be in use. + /// + public bool AllowContentDashboardAccessToAllUsers + { + get + { + try + { + return bool.Parse(ConfigurationManager.AppSettings[Constants.AppSettings.AllowContentDashboardAccessToAllUsers]); + } + catch + { + return false; + } + } + } + /// /// An int value representing the time in milliseconds to lock the database for a write operation diff --git a/src/Umbraco.Core/Configuration/IGlobalSettings.cs b/src/Umbraco.Core/Configuration/IGlobalSettings.cs index 483829f85f..6016de2917 100644 --- a/src/Umbraco.Core/Configuration/IGlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/IGlobalSettings.cs @@ -57,6 +57,15 @@ /// bool UseHttps { get; } + /// + /// Gets a value indicating whether the content dashboard should be available to all users. + /// + /// + /// true if the dashboard is visible for all user groups; otherwise, false + /// and the default access rules for that dashboard will be in use. + /// + bool AllowContentDashboardAccessToAllUsers { get; } + /// /// Returns a string value to determine if umbraco should skip version-checking. /// diff --git a/src/Umbraco.Core/Constants-AppSettings.cs b/src/Umbraco.Core/Constants-AppSettings.cs index f04f0e1f5f..1f096ab9f9 100644 --- a/src/Umbraco.Core/Constants-AppSettings.cs +++ b/src/Umbraco.Core/Constants-AppSettings.cs @@ -110,6 +110,11 @@ namespace Umbraco.Core /// public const string UseHttps = "Umbraco.Core.UseHttps"; + /// + /// A true/false value indicating whether the content dashboard should be visible for all user groups. + /// + public const string AllowContentDashboardAccessToAllUsers = "Umbraco.Core.AllowContentDashboardAccessToAllUsers"; + /// /// TODO: FILL ME IN /// diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 7fb27deb7e..eb6649a7c4 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -148,7 +148,6 @@ True Settings.settings - diff --git a/src/Umbraco.Web.UI/config/content.dashboard.access.config.js b/src/Umbraco.Web.UI/config/content.dashboard.access.config.js deleted file mode 100644 index 93c727d85a..0000000000 --- a/src/Umbraco.Web.UI/config/content.dashboard.access.config.js +++ /dev/null @@ -1,22 +0,0 @@ -[ - { - "Type": "grant", - "Value": "admin" - }, - { - "Type": "grant", - "Value": "editor" - }, - { - "Type": "grant", - "Value": "sensitiveData" - }, - { - "Type": "grant", - "Value": "translator" - }, - { - "Type": "grant", - "Value": "writer" - } -] diff --git a/src/Umbraco.Web.UI/web.Template.config b/src/Umbraco.Web.UI/web.Template.config index 03f462fb9e..ae141e5408 100644 --- a/src/Umbraco.Web.UI/web.Template.config +++ b/src/Umbraco.Web.UI/web.Template.config @@ -37,6 +37,7 @@ + From 6e54c6fefde685a0d10fe9a2fdf5c82177b8c0b0 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Wed, 24 Mar 2021 15:34:05 +0100 Subject: [PATCH 41/78] Implementation for IContentDashboardSettings --- src/Umbraco.Core/ConfigsExtensions.cs | 6 +++ .../Dashboards/ContentDashboardSettings.cs | 43 +++++++++++++++++++ .../Dashboards/IContentDashboardSettings.cs | 13 ++++++ src/Umbraco.Core/Umbraco.Core.csproj | 2 + .../Dashboards/ContentDashboard.cs | 17 ++++---- 5 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 src/Umbraco.Core/Dashboards/ContentDashboardSettings.cs create mode 100644 src/Umbraco.Core/Dashboards/IContentDashboardSettings.cs diff --git a/src/Umbraco.Core/ConfigsExtensions.cs b/src/Umbraco.Core/ConfigsExtensions.cs index d1672c6c7f..17dc63943a 100644 --- a/src/Umbraco.Core/ConfigsExtensions.cs +++ b/src/Umbraco.Core/ConfigsExtensions.cs @@ -5,9 +5,11 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Grid; using Umbraco.Core.Configuration.HealthChecks; using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Dashboards; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Manifest; +using Umbraco.Core.Services; namespace Umbraco.Core { @@ -48,6 +50,10 @@ namespace Umbraco.Core configDir, factory.GetInstance(), factory.GetInstance().Debug)); + + configs.Add(factory => + new ContentDashboardSettings(factory.GetInstance(), + factory.GetInstance())); } } } diff --git a/src/Umbraco.Core/Dashboards/ContentDashboardSettings.cs b/src/Umbraco.Core/Dashboards/ContentDashboardSettings.cs new file mode 100644 index 0000000000..3db808fe02 --- /dev/null +++ b/src/Umbraco.Core/Dashboards/ContentDashboardSettings.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Configuration; +using Umbraco.Core.Services; + +namespace Umbraco.Core.Dashboards +{ + public class ContentDashboardSettings: IContentDashboardSettings + { + private readonly IGlobalSettings _globalSettings; + private readonly IUserService _userService; + + public ContentDashboardSettings(IGlobalSettings globalSettings, IUserService userService) + { + _globalSettings = globalSettings; + _userService = userService; + } + + public IAccessRule[] GetAccessRulesFromConfig() + { + var rules = new List(); + + if (_globalSettings.AllowContentDashboardAccessToAllUsers) + { + var allUserGroups = _userService.GetAllUserGroups(); + + foreach (var userGroup in allUserGroups) + { + rules.Add(new AccessRule + { + Type = AccessRuleType.Grant, + Value = userGroup.Alias + }); + } + } + + return rules.ToArray(); + } + } +} diff --git a/src/Umbraco.Core/Dashboards/IContentDashboardSettings.cs b/src/Umbraco.Core/Dashboards/IContentDashboardSettings.cs new file mode 100644 index 0000000000..9b5ea7d7dd --- /dev/null +++ b/src/Umbraco.Core/Dashboards/IContentDashboardSettings.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Core.Dashboards +{ + public interface IContentDashboardSettings + { + IAccessRule[] GetAccessRulesFromConfig(); + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 832b8a5801..63c7ac178d 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -131,6 +131,8 @@ + + diff --git a/src/Umbraco.Web/Dashboards/ContentDashboard.cs b/src/Umbraco.Web/Dashboards/ContentDashboard.cs index 3dbc0cb693..4b17bcf7ff 100644 --- a/src/Umbraco.Web/Dashboards/ContentDashboard.cs +++ b/src/Umbraco.Web/Dashboards/ContentDashboard.cs @@ -10,6 +10,7 @@ namespace Umbraco.Web.Dashboards [Weight(10)] public class ContentDashboard : IDashboard { + private readonly IContentDashboardSettings _dashboardSettings; public string Alias => "contentIntro"; public string[] Sections => new[] { "content" }; @@ -20,15 +21,9 @@ namespace Umbraco.Web.Dashboards { get { - IAccessRule[] rules; - var dashboardConfig = Path.Combine(IOHelper.MapPath(SystemDirectories.Config), "content.dashboard.access.config.js"); + var rules = _dashboardSettings.GetAccessRulesFromConfig(); - if (File.Exists(dashboardConfig)) - { - var rawJson = File.ReadAllText(dashboardConfig); - rules = JsonConvert.DeserializeObject(rawJson); - } - else + if (rules.Length == 0) { rules = new IAccessRule[] { @@ -36,8 +31,14 @@ namespace Umbraco.Web.Dashboards new AccessRule {Type = AccessRuleType.Grant, Value = Constants.Security.AdminGroupAlias} }; } + return rules; } } + + public ContentDashboard(IContentDashboardSettings dashboardSettings) + { + _dashboardSettings = dashboardSettings; + } } } From 61f486ebeaac426a6fc304d0bebbab9cc35d15e6 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Wed, 24 Mar 2021 15:36:29 +0100 Subject: [PATCH 42/78] Cleanup --- src/Umbraco.Core/ConfigsExtensions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Core/ConfigsExtensions.cs b/src/Umbraco.Core/ConfigsExtensions.cs index 17dc63943a..92d03adadb 100644 --- a/src/Umbraco.Core/ConfigsExtensions.cs +++ b/src/Umbraco.Core/ConfigsExtensions.cs @@ -1,6 +1,5 @@ using System.IO; using Umbraco.Core.Cache; -using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Grid; using Umbraco.Core.Configuration.HealthChecks; From 56d5704167e940c540ae67864cdb5263b9b535f3 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Wed, 24 Mar 2021 16:15:21 +0100 Subject: [PATCH 43/78] bool.Try --- src/Umbraco.Core/Configuration/GlobalSettings.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/Configuration/GlobalSettings.cs b/src/Umbraco.Core/Configuration/GlobalSettings.cs index 0765a35a29..a451018169 100644 --- a/src/Umbraco.Core/Configuration/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/GlobalSettings.cs @@ -406,14 +406,8 @@ namespace Umbraco.Core.Configuration { get { - try - { - return bool.Parse(ConfigurationManager.AppSettings[Constants.AppSettings.AllowContentDashboardAccessToAllUsers]); - } - catch - { - return false; - } + bool.TryParse(ConfigurationManager.AppSettings[Constants.AppSettings.AllowContentDashboardAccessToAllUsers], out var value); + return value; } } From 18083b715513887ee7df5ac2df5d4a116b632900 Mon Sep 17 00:00:00 2001 From: Mole Date: Thu, 25 Mar 2021 09:00:38 +0100 Subject: [PATCH 44/78] Switch LocalizationService over to event aggregator --- .../Services/Implement/LocalizationService.cs | 52 ++++++++++++------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs b/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs index abdda2e68c..f4072ccfb7 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs @@ -6,6 +6,7 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Services.Notifications; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Services.Implement @@ -17,15 +18,23 @@ namespace Umbraco.Cms.Core.Services.Implement { private readonly IDictionaryRepository _dictionaryRepository; private readonly ILanguageRepository _languageRepository; + private readonly IEventAggregator _eventAggregator; private readonly IAuditRepository _auditRepository; - public LocalizationService(IScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, - IDictionaryRepository dictionaryRepository, IAuditRepository auditRepository, ILanguageRepository languageRepository) + public LocalizationService( + IScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IDictionaryRepository dictionaryRepository, + IAuditRepository auditRepository, + ILanguageRepository languageRepository, + IEventAggregator eventAggregator) : base(provider, loggerFactory, eventMessagesFactory) { _dictionaryRepository = dictionaryRepository; _auditRepository = auditRepository; _languageRepository = languageRepository; + _eventAggregator = eventAggregator; } /// @@ -88,9 +97,11 @@ namespace Umbraco.Cms.Core.Services.Implement item.Translations = translations; } - var saveEventArgs = new SaveEventArgs(item); - if (scope.Events.DispatchCancelable(SavingDictionaryItem, this, saveEventArgs)) + EventMessages eventMessages = EventMessagesFactory.Get(); + var savingNotification = new DictionaryItemSavingNotification(item, eventMessages); + + if (_eventAggregator.PublishCancelable(savingNotification)) { scope.Complete(); return item; @@ -100,8 +111,7 @@ namespace Umbraco.Cms.Core.Services.Implement // ensure the lazy Language callback is assigned EnsureDictionaryItemLanguageCallback(item); - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(SavedDictionaryItem, this, saveEventArgs); + _eventAggregator.Publish(new DictionaryItemSavedNotification(item, eventMessages).WithStateFrom(savingNotification)); scope.Complete(); @@ -232,7 +242,9 @@ namespace Umbraco.Cms.Core.Services.Implement { using (var scope = ScopeProvider.CreateScope()) { - if (scope.Events.DispatchCancelable(SavingDictionaryItem, this, new SaveEventArgs(dictionaryItem))) + EventMessages eventMessages = EventMessagesFactory.Get(); + var savingNotification = new DictionaryItemSavingNotification(dictionaryItem, eventMessages); + if (_eventAggregator.PublishCancelable(savingNotification)) { scope.Complete(); return; @@ -244,7 +256,7 @@ namespace Umbraco.Cms.Core.Services.Implement // ensure the lazy Language callback is assigned EnsureDictionaryItemLanguageCallback(dictionaryItem); - scope.Events.Dispatch(SavedDictionaryItem, this, new SaveEventArgs(dictionaryItem, false)); + _eventAggregator.Publish(new DictionaryItemSavedNotification(dictionaryItem, eventMessages).WithStateFrom(savingNotification)); Audit(AuditType.Save, "Save DictionaryItem", userId, dictionaryItem.Id, "DictionaryItem"); scope.Complete(); @@ -261,16 +273,16 @@ namespace Umbraco.Cms.Core.Services.Implement { using (var scope = ScopeProvider.CreateScope()) { - var deleteEventArgs = new DeleteEventArgs(dictionaryItem); - if (scope.Events.DispatchCancelable(DeletingDictionaryItem, this, deleteEventArgs)) + EventMessages eventMessages = EventMessagesFactory.Get(); + var deletingNotification = new DictionaryItemDeletingNotification(dictionaryItem, eventMessages); + if (_eventAggregator.PublishCancelable(deletingNotification)) { scope.Complete(); return; } _dictionaryRepository.Delete(dictionaryItem); - deleteEventArgs.CanCancel = false; - scope.Events.Dispatch(DeletedDictionaryItem, this, deleteEventArgs); + _eventAggregator.Publish(new DictionaryItemDeletedNotification(dictionaryItem, eventMessages).WithStateFrom(deletingNotification)); Audit(AuditType.Delete, "Delete DictionaryItem", userId, dictionaryItem.Id, "DictionaryItem"); @@ -374,16 +386,16 @@ namespace Umbraco.Cms.Core.Services.Implement throw new InvalidOperationException($"Cannot save language {language.IsoCode} with fallback {languages[language.FallbackLanguageId.Value].IsoCode} as it would create a fallback cycle."); } - var saveEventArgs = new SaveEventArgs(language); - if (scope.Events.DispatchCancelable(SavingLanguage, this, saveEventArgs)) + EventMessages eventMessages = EventMessagesFactory.Get(); + var savingNotification = new LanguageSavingNotification(language, eventMessages); + if (_eventAggregator.PublishCancelable(savingNotification)) { scope.Complete(); return; } _languageRepository.Save(language); - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(SavedLanguage, this, saveEventArgs); + _eventAggregator.Publish(new LanguageSavedNotification(language, eventMessages).WithStateFrom(savingNotification)); Audit(AuditType.Save, "Save Language", userId, language.Id, ObjectTypes.GetName(UmbracoObjectTypes.Language)); @@ -417,8 +429,9 @@ namespace Umbraco.Cms.Core.Services.Implement // write-lock languages to guard against race conds when dealing with default language scope.WriteLock(Cms.Core.Constants.Locks.Languages); - var deleteEventArgs = new DeleteEventArgs(language); - if (scope.Events.DispatchCancelable(DeletingLanguage, this, deleteEventArgs)) + EventMessages eventMessages = EventMessagesFactory.Get(); + var deletingLanguageNotification = new LanguageDeletingNotification(language, eventMessages); + if (_eventAggregator.PublishCancelable(deletingLanguageNotification)) { scope.Complete(); return; @@ -426,9 +439,8 @@ namespace Umbraco.Cms.Core.Services.Implement // NOTE: Other than the fall-back language, there aren't any other constraints in the db, so possible references aren't deleted _languageRepository.Delete(language); - deleteEventArgs.CanCancel = false; - scope.Events.Dispatch(DeletedLanguage, this, deleteEventArgs); + _eventAggregator.Publish(new LanguageDeletedNotification(language, eventMessages).WithStateFrom(deletingLanguageNotification)); Audit(AuditType.Delete, "Delete Language", userId, language.Id, ObjectTypes.GetName(UmbracoObjectTypes.Language)); scope.Complete(); From f68d4d6968358c0c7aefe75aa2324cd591511005 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Thu, 25 Mar 2021 11:11:41 +0100 Subject: [PATCH 45/78] Taking AllowContentDashboardAccessToAllUsers prop from GlobalSettings to ContentDashboardSettings and saving AccessRulesFromConfig into a backing field --- src/Umbraco.Core/ConfigsExtensions.cs | 5 +-- .../Configuration/GlobalSettings.cs | 17 -------- .../Configuration/IGlobalSettings.cs | 9 ---- .../Dashboards/ContentDashboardSettings.cs | 43 ++++++------------- .../Dashboards/IContentDashboardSettings.cs | 17 ++++---- .../Dashboards/ContentDashboard.cs | 42 +++++++++++++++--- 6 files changed, 59 insertions(+), 74 deletions(-) diff --git a/src/Umbraco.Core/ConfigsExtensions.cs b/src/Umbraco.Core/ConfigsExtensions.cs index 92d03adadb..10594fc970 100644 --- a/src/Umbraco.Core/ConfigsExtensions.cs +++ b/src/Umbraco.Core/ConfigsExtensions.cs @@ -8,7 +8,6 @@ using Umbraco.Core.Dashboards; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Manifest; -using Umbraco.Core.Services; namespace Umbraco.Core { @@ -50,9 +49,7 @@ namespace Umbraco.Core factory.GetInstance(), factory.GetInstance().Debug)); - configs.Add(factory => - new ContentDashboardSettings(factory.GetInstance(), - factory.GetInstance())); + configs.Add(() => new ContentDashboardSettings()); } } } diff --git a/src/Umbraco.Core/Configuration/GlobalSettings.cs b/src/Umbraco.Core/Configuration/GlobalSettings.cs index a451018169..c844abe75e 100644 --- a/src/Umbraco.Core/Configuration/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/GlobalSettings.cs @@ -395,23 +395,6 @@ namespace Umbraco.Core.Configuration } } - /// - /// Gets a value indicating whether the content dashboard should be available to all users. - /// - /// - /// true if the dashboard is visible for all user groups; otherwise, false - /// and the default access rules for that dashboard will be in use. - /// - public bool AllowContentDashboardAccessToAllUsers - { - get - { - bool.TryParse(ConfigurationManager.AppSettings[Constants.AppSettings.AllowContentDashboardAccessToAllUsers], out var value); - return value; - } - } - - /// /// An int value representing the time in milliseconds to lock the database for a write operation /// diff --git a/src/Umbraco.Core/Configuration/IGlobalSettings.cs b/src/Umbraco.Core/Configuration/IGlobalSettings.cs index 6016de2917..483829f85f 100644 --- a/src/Umbraco.Core/Configuration/IGlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/IGlobalSettings.cs @@ -57,15 +57,6 @@ /// bool UseHttps { get; } - /// - /// Gets a value indicating whether the content dashboard should be available to all users. - /// - /// - /// true if the dashboard is visible for all user groups; otherwise, false - /// and the default access rules for that dashboard will be in use. - /// - bool AllowContentDashboardAccessToAllUsers { get; } - /// /// Returns a string value to determine if umbraco should skip version-checking. /// diff --git a/src/Umbraco.Core/Dashboards/ContentDashboardSettings.cs b/src/Umbraco.Core/Dashboards/ContentDashboardSettings.cs index 3db808fe02..f8fb5c7b06 100644 --- a/src/Umbraco.Core/Dashboards/ContentDashboardSettings.cs +++ b/src/Umbraco.Core/Dashboards/ContentDashboardSettings.cs @@ -1,43 +1,24 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Umbraco.Core.Configuration; -using Umbraco.Core.Services; +using System.Configuration; namespace Umbraco.Core.Dashboards { public class ContentDashboardSettings: IContentDashboardSettings { - private readonly IGlobalSettings _globalSettings; - private readonly IUserService _userService; - public ContentDashboardSettings(IGlobalSettings globalSettings, IUserService userService) + /// + /// Gets a value indicating whether the content dashboard should be available to all users. + /// + /// + /// true if the dashboard is visible for all user groups; otherwise, false + /// and the default access rules for that dashboard will be in use. + /// + public bool AllowContentDashboardAccessToAllUsers { - _globalSettings = globalSettings; - _userService = userService; - } - - public IAccessRule[] GetAccessRulesFromConfig() - { - var rules = new List(); - - if (_globalSettings.AllowContentDashboardAccessToAllUsers) + get { - var allUserGroups = _userService.GetAllUserGroups(); - - foreach (var userGroup in allUserGroups) - { - rules.Add(new AccessRule - { - Type = AccessRuleType.Grant, - Value = userGroup.Alias - }); - } + bool.TryParse(ConfigurationManager.AppSettings[Constants.AppSettings.AllowContentDashboardAccessToAllUsers], out var value); + return value; } - - return rules.ToArray(); } } } diff --git a/src/Umbraco.Core/Dashboards/IContentDashboardSettings.cs b/src/Umbraco.Core/Dashboards/IContentDashboardSettings.cs index 9b5ea7d7dd..862a28b90e 100644 --- a/src/Umbraco.Core/Dashboards/IContentDashboardSettings.cs +++ b/src/Umbraco.Core/Dashboards/IContentDashboardSettings.cs @@ -1,13 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Umbraco.Core.Dashboards +namespace Umbraco.Core.Dashboards { public interface IContentDashboardSettings { - IAccessRule[] GetAccessRulesFromConfig(); + /// + /// Gets a value indicating whether the content dashboard should be available to all users. + /// + /// + /// true if the dashboard is visible for all user groups; otherwise, false + /// and the default access rules for that dashboard will be in use. + /// + bool AllowContentDashboardAccessToAllUsers { get; } } } diff --git a/src/Umbraco.Web/Dashboards/ContentDashboard.cs b/src/Umbraco.Web/Dashboards/ContentDashboard.cs index 4b17bcf7ff..260eb8baf9 100644 --- a/src/Umbraco.Web/Dashboards/ContentDashboard.cs +++ b/src/Umbraco.Web/Dashboards/ContentDashboard.cs @@ -1,9 +1,8 @@ -using System.IO; -using Newtonsoft.Json; +using System.Collections.Generic; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Dashboards; -using Umbraco.Core.IO; +using Umbraco.Core.Services; namespace Umbraco.Web.Dashboards { @@ -11,6 +10,9 @@ namespace Umbraco.Web.Dashboards public class ContentDashboard : IDashboard { private readonly IContentDashboardSettings _dashboardSettings; + private readonly IUserService _userService; + private IAccessRule[] _accessRulesFromConfig; + public string Alias => "contentIntro"; public string[] Sections => new[] { "content" }; @@ -21,7 +23,7 @@ namespace Umbraco.Web.Dashboards { get { - var rules = _dashboardSettings.GetAccessRulesFromConfig(); + var rules = AccessRulesFromConfig; if (rules.Length == 0) { @@ -36,9 +38,39 @@ namespace Umbraco.Web.Dashboards } } - public ContentDashboard(IContentDashboardSettings dashboardSettings) + private IAccessRule[] AccessRulesFromConfig + { + get + { + if (_accessRulesFromConfig is null) + { + var rules = new List(); + + if (_dashboardSettings.AllowContentDashboardAccessToAllUsers) + { + var allUserGroups = _userService.GetAllUserGroups(); + + foreach (var userGroup in allUserGroups) + { + rules.Add(new AccessRule + { + Type = AccessRuleType.Grant, + Value = userGroup.Alias + }); + } + } + + _accessRulesFromConfig = rules.ToArray(); + } + + return _accessRulesFromConfig; + } + } + + public ContentDashboard(IContentDashboardSettings dashboardSettings, IUserService userService) { _dashboardSettings = dashboardSettings; + _userService = userService; } } } From c8471b096cc288b5d8d42dbc489b2b520230f128 Mon Sep 17 00:00:00 2001 From: Mole Date: Thu, 25 Mar 2021 14:51:00 +0100 Subject: [PATCH 46/78] Switch to INotificationHandler --- .../Cache/DistributedCacheBinder_Handlers.cs | 50 +++++++------- .../Compose/NotificationsComposer.cs | 8 +++ .../Services/Implement/LocalizationService.cs | 67 +++---------------- .../Compose/NotificationsComposer.cs | 17 +++++ .../PublishedSnapshotServiceEventHandler.cs | 12 ++-- .../Scoping/ScopedRepositoryTests.cs | 22 ++++-- 6 files changed, 82 insertions(+), 94 deletions(-) create mode 100644 src/Umbraco.PublishedCache.NuCache/Compose/NotificationsComposer.cs diff --git a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs index 1aa4906029..de75ac0905 100644 --- a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs +++ b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs @@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Changes; using Umbraco.Cms.Core.Services.Implement; +using Umbraco.Cms.Infrastructure.Services.Notifications; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Cache @@ -18,7 +19,11 @@ namespace Umbraco.Cms.Core.Cache /// /// Default implementation. /// - public partial class DistributedCacheBinder + public partial class DistributedCacheBinder : + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler { private List _unbinders; @@ -61,12 +66,6 @@ namespace Umbraco.Cms.Core.Cache Bind(() => UserService.UserGroupPermissionsAssigned += UserService_UserGroupPermissionsAssigned, () => UserService.UserGroupPermissionsAssigned -= UserService_UserGroupPermissionsAssigned); - // bind to dictionary events - Bind(() => LocalizationService.DeletedDictionaryItem += LocalizationService_DeletedDictionaryItem, - () => LocalizationService.DeletedDictionaryItem -= LocalizationService_DeletedDictionaryItem); - Bind(() => LocalizationService.SavedDictionaryItem += LocalizationService_SavedDictionaryItem, - () => LocalizationService.SavedDictionaryItem -= LocalizationService_SavedDictionaryItem); - // bind to data type events Bind(() => DataTypeService.Deleted += DataTypeService_Deleted, () => DataTypeService.Deleted -= DataTypeService_Deleted); @@ -85,12 +84,6 @@ namespace Umbraco.Cms.Core.Cache Bind(() => DomainService.Deleted += DomainService_Deleted, () => DomainService.Deleted -= DomainService_Deleted); - // bind to language events - Bind(() => LocalizationService.SavedLanguage += LocalizationService_SavedLanguage, - () => LocalizationService.SavedLanguage -= LocalizationService_SavedLanguage); - Bind(() => LocalizationService.DeletedLanguage += LocalizationService_DeletedLanguage, - () => LocalizationService.DeletedLanguage -= LocalizationService_DeletedLanguage); - // bind to content type events Bind(() => ContentTypeService.Changed += ContentTypeService_Changed, () => ContentTypeService.Changed -= ContentTypeService_Changed); @@ -196,17 +189,20 @@ namespace Umbraco.Cms.Core.Cache #endregion #region LocalizationService / Dictionary - - private void LocalizationService_SavedDictionaryItem(ILocalizationService sender, SaveEventArgs e) + public void Handle(DictionaryItemSavedNotification notification) { - foreach (var entity in e.SavedEntities) + foreach (IDictionaryItem entity in notification.SavedEntities) + { _distributedCache.RefreshDictionaryCache(entity.Id); + } } - private void LocalizationService_DeletedDictionaryItem(ILocalizationService sender, DeleteEventArgs e) + public void Handle(DictionaryItemDeletedNotification notification) { - foreach (var entity in e.DeletedEntities) + foreach (IDictionaryItem entity in notification.DeletedEntities) + { _distributedCache.RemoveDictionaryCache(entity.Id); + } } #endregion @@ -248,23 +244,25 @@ namespace Umbraco.Cms.Core.Cache /// /// Fires when a language is deleted /// - /// - /// - private void LocalizationService_DeletedLanguage(ILocalizationService sender, DeleteEventArgs e) + /// + public void Handle(LanguageDeletedNotification notification) { - foreach (var entity in e.DeletedEntities) + foreach (ILanguage entity in notification.DeletedEntities) + { _distributedCache.RemoveLanguageCache(entity); + } } /// /// Fires when a language is saved /// - /// - /// - private void LocalizationService_SavedLanguage(ILocalizationService sender, SaveEventArgs e) + /// + public void Handle(LanguageSavedNotification notification) { - foreach (var entity in e.SavedEntities) + foreach (ILanguage entity in notification.SavedEntities) + { _distributedCache.RefreshLanguageCache(entity); + } } #endregion diff --git a/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs b/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs index c760c33b71..2e3403e3dd 100644 --- a/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs +++ b/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs @@ -1,6 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; @@ -62,6 +63,13 @@ namespace Umbraco.Cms.Core.Compose .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler(); + + // Add notification handlers for DistributedCache + builder + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler(); } } } diff --git a/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs b/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs index f4072ccfb7..b8c36f1ed9 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs @@ -18,7 +18,6 @@ namespace Umbraco.Cms.Core.Services.Implement { private readonly IDictionaryRepository _dictionaryRepository; private readonly ILanguageRepository _languageRepository; - private readonly IEventAggregator _eventAggregator; private readonly IAuditRepository _auditRepository; public LocalizationService( @@ -27,14 +26,12 @@ namespace Umbraco.Cms.Core.Services.Implement IEventMessagesFactory eventMessagesFactory, IDictionaryRepository dictionaryRepository, IAuditRepository auditRepository, - ILanguageRepository languageRepository, - IEventAggregator eventAggregator) + ILanguageRepository languageRepository) : base(provider, loggerFactory, eventMessagesFactory) { _dictionaryRepository = dictionaryRepository; _auditRepository = auditRepository; _languageRepository = languageRepository; - _eventAggregator = eventAggregator; } /// @@ -101,7 +98,7 @@ namespace Umbraco.Cms.Core.Services.Implement EventMessages eventMessages = EventMessagesFactory.Get(); var savingNotification = new DictionaryItemSavingNotification(item, eventMessages); - if (_eventAggregator.PublishCancelable(savingNotification)) + if (scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); return item; @@ -111,7 +108,7 @@ namespace Umbraco.Cms.Core.Services.Implement // ensure the lazy Language callback is assigned EnsureDictionaryItemLanguageCallback(item); - _eventAggregator.Publish(new DictionaryItemSavedNotification(item, eventMessages).WithStateFrom(savingNotification)); + scope.Notifications.Publish(new DictionaryItemSavedNotification(item, eventMessages).WithStateFrom(savingNotification)); scope.Complete(); @@ -244,7 +241,7 @@ namespace Umbraco.Cms.Core.Services.Implement { EventMessages eventMessages = EventMessagesFactory.Get(); var savingNotification = new DictionaryItemSavingNotification(dictionaryItem, eventMessages); - if (_eventAggregator.PublishCancelable(savingNotification)) + if (scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); return; @@ -256,7 +253,7 @@ namespace Umbraco.Cms.Core.Services.Implement // ensure the lazy Language callback is assigned EnsureDictionaryItemLanguageCallback(dictionaryItem); - _eventAggregator.Publish(new DictionaryItemSavedNotification(dictionaryItem, eventMessages).WithStateFrom(savingNotification)); + scope.Notifications.Publish(new DictionaryItemSavedNotification(dictionaryItem, eventMessages).WithStateFrom(savingNotification)); Audit(AuditType.Save, "Save DictionaryItem", userId, dictionaryItem.Id, "DictionaryItem"); scope.Complete(); @@ -275,14 +272,14 @@ namespace Umbraco.Cms.Core.Services.Implement { EventMessages eventMessages = EventMessagesFactory.Get(); var deletingNotification = new DictionaryItemDeletingNotification(dictionaryItem, eventMessages); - if (_eventAggregator.PublishCancelable(deletingNotification)) + if (scope.Notifications.PublishCancelable(deletingNotification)) { scope.Complete(); return; } _dictionaryRepository.Delete(dictionaryItem); - _eventAggregator.Publish(new DictionaryItemDeletedNotification(dictionaryItem, eventMessages).WithStateFrom(deletingNotification)); + scope.Notifications.Publish(new DictionaryItemDeletedNotification(dictionaryItem, eventMessages).WithStateFrom(deletingNotification)); Audit(AuditType.Delete, "Delete DictionaryItem", userId, dictionaryItem.Id, "DictionaryItem"); @@ -388,14 +385,14 @@ namespace Umbraco.Cms.Core.Services.Implement EventMessages eventMessages = EventMessagesFactory.Get(); var savingNotification = new LanguageSavingNotification(language, eventMessages); - if (_eventAggregator.PublishCancelable(savingNotification)) + if (scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); return; } _languageRepository.Save(language); - _eventAggregator.Publish(new LanguageSavedNotification(language, eventMessages).WithStateFrom(savingNotification)); + scope.Notifications.Publish(new LanguageSavedNotification(language, eventMessages).WithStateFrom(savingNotification)); Audit(AuditType.Save, "Save Language", userId, language.Id, ObjectTypes.GetName(UmbracoObjectTypes.Language)); @@ -431,7 +428,7 @@ namespace Umbraco.Cms.Core.Services.Implement EventMessages eventMessages = EventMessagesFactory.Get(); var deletingLanguageNotification = new LanguageDeletingNotification(language, eventMessages); - if (_eventAggregator.PublishCancelable(deletingLanguageNotification)) + if (scope.Notifications.PublishCancelable(deletingLanguageNotification)) { scope.Complete(); return; @@ -440,7 +437,7 @@ namespace Umbraco.Cms.Core.Services.Implement // NOTE: Other than the fall-back language, there aren't any other constraints in the db, so possible references aren't deleted _languageRepository.Delete(language); - _eventAggregator.Publish(new LanguageDeletedNotification(language, eventMessages).WithStateFrom(deletingLanguageNotification)); + scope.Notifications.Publish(new LanguageDeletedNotification(language, eventMessages).WithStateFrom(deletingLanguageNotification)); Audit(AuditType.Delete, "Delete Language", userId, language.Id, ObjectTypes.GetName(UmbracoObjectTypes.Language)); scope.Complete(); @@ -475,47 +472,5 @@ namespace Umbraco.Cms.Core.Services.Implement return _dictionaryRepository.GetDictionaryItemKeyMap(); } } - - #region Events - /// - /// Occurs before Delete - /// - public static event TypedEventHandler> DeletingLanguage; - - /// - /// Occurs after Delete - /// - public static event TypedEventHandler> DeletedLanguage; - - /// - /// Occurs before Delete - /// - public static event TypedEventHandler> DeletingDictionaryItem; - - /// - /// Occurs after Delete - /// - public static event TypedEventHandler> DeletedDictionaryItem; - - /// - /// Occurs before Save - /// - public static event TypedEventHandler> SavingDictionaryItem; - - /// - /// Occurs after Save - /// - public static event TypedEventHandler> SavedDictionaryItem; - - /// - /// Occurs before Save - /// - public static event TypedEventHandler> SavingLanguage; - - /// - /// Occurs after Save - /// - public static event TypedEventHandler> SavedLanguage; - #endregion } } diff --git a/src/Umbraco.PublishedCache.NuCache/Compose/NotificationsComposer.cs b/src/Umbraco.PublishedCache.NuCache/Compose/NotificationsComposer.cs new file mode 100644 index 0000000000..df84759793 --- /dev/null +++ b/src/Umbraco.PublishedCache.NuCache/Compose/NotificationsComposer.cs @@ -0,0 +1,17 @@ +using Umbraco.Cms.Core.Compose; +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Infrastructure.Services.Notifications; + +namespace Umbraco.Cms.Infrastructure.PublishedCache.Compose +{ + public sealed class NotificationsComposer : ComponentComposer, ICoreComposer + { + public override void Compose(IUmbracoBuilder builder) + { + base.Compose(builder); + + builder.AddNotificationHandler(); + } + } +} diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs index 25ceb9fb6a..df02320d87 100644 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs +++ b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs @@ -9,6 +9,7 @@ using Umbraco.Cms.Core.Services.Changes; using Umbraco.Cms.Core.Services.Implement; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; using Umbraco.Cms.Infrastructure.PublishedCache.Persistence; +using Umbraco.Cms.Infrastructure.Services.Notifications; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.PublishedCache @@ -16,7 +17,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache /// /// Subscribes to Umbraco events to ensure nucache remains consistent with the source data /// - public class PublishedSnapshotServiceEventHandler : IDisposable + public class PublishedSnapshotServiceEventHandler : IDisposable, INotificationHandler { private readonly IRuntimeState _runtime; private bool _disposedValue; @@ -79,9 +80,6 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache ContentTypeService.ScopedRefreshedEntity += OnContentTypeRefreshedEntity; MediaTypeService.ScopedRefreshedEntity += OnMediaTypeRefreshedEntity; MemberTypeService.ScopedRefreshedEntity += OnMemberTypeRefreshedEntity; - - // TODO: This should be a cache refresher call! - LocalizationService.SavedLanguage += OnLanguageSaved; } private void TearDownRepositoryEvents() @@ -95,7 +93,6 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache ContentTypeService.ScopedRefreshedEntity -= OnContentTypeRefreshedEntity; MediaTypeService.ScopedRefreshedEntity -= OnMediaTypeRefreshedEntity; MemberTypeService.ScopedRefreshedEntity -= OnMemberTypeRefreshedEntity; - LocalizationService.SavedLanguage -= OnLanguageSaved; // TODO: Shouldn't this be a cache refresher event? } // note: if the service is not ready, ie _isReady is false, then we still handle repository events, @@ -156,13 +153,14 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache } } + // TODO: This should be a cache refresher call! /// /// If a is ever saved with a different culture, we need to rebuild all of the content nucache database table /// - private void OnLanguageSaved(ILocalizationService sender, SaveEventArgs e) + public void Handle(LanguageSavedNotification notification) { // culture changed on an existing language - var cultureChanged = e.SavedEntities.Any(x => !x.WasPropertyDirty(nameof(ILanguage.Id)) && x.WasPropertyDirty(nameof(ILanguage.IsoCode))); + var cultureChanged = notification.SavedEntities.Any(x => !x.WasPropertyDirty(nameof(ILanguage.Id)) && x.WasPropertyDirty(nameof(ILanguage.IsoCode))); if (cultureChanged) { // Rebuild all content for all content types diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs index 96ba30905b..fcfd81a39d 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs @@ -6,6 +6,7 @@ using System.Linq; using Microsoft.Extensions.Logging; using NUnit.Framework; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; @@ -14,9 +15,12 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Implement; using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Infrastructure.PublishedCache; +using Umbraco.Cms.Infrastructure.Services.Notifications; using Umbraco.Cms.Infrastructure.Sync; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; +using Umbraco.Extensions; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping { @@ -44,6 +48,17 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping return result; } + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + builder.Services.AddUnique(); + builder + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler(); + builder.AddNotificationHandler(); + } + [TearDown] public void Teardown() { @@ -154,9 +169,6 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping Assert.AreEqual(lang.Id, globalCached.Id); Assert.AreEqual("fr-FR", globalCached.IsoCode); - _distributedCacheBinder = new DistributedCacheBinder(new DistributedCache(ServerMessenger, CacheRefresherCollection), GetRequiredService(), GetRequiredService>()); - _distributedCacheBinder.BindEvents(true); - Assert.IsNull(scopeProvider.AmbientScope); using (IScope scope = scopeProvider.CreateScope(repositoryCacheMode: RepositoryCacheMode.Scoped)) { @@ -250,8 +262,8 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping Assert.AreEqual(item.Id, globalCached.Id); Assert.AreEqual("item-key", globalCached.ItemKey); - _distributedCacheBinder = new DistributedCacheBinder(new DistributedCache(ServerMessenger, CacheRefresherCollection), GetRequiredService(), GetRequiredService>()); - _distributedCacheBinder.BindEvents(true); + // _distributedCacheBinder = new DistributedCacheBinder(new DistributedCache(ServerMessenger, CacheRefresherCollection), GetRequiredService(), GetRequiredService>()); + // _distributedCacheBinder.BindEvents(true); Assert.IsNull(scopeProvider.AmbientScope); using (IScope scope = scopeProvider.CreateScope(repositoryCacheMode: RepositoryCacheMode.Scoped)) From 6a81b31d847fa5627496083ddce8bd80f1c388ab Mon Sep 17 00:00:00 2001 From: Mole Date: Thu, 25 Mar 2021 15:54:43 +0100 Subject: [PATCH 47/78] Tentative fix for ScopedRepositoryTests Caches are getting cleared because we can't unbind from events :( --- .../Cache/DistributedCacheBinderTests.cs | 6 ------ .../Scoping/ScopedRepositoryTests.cs | 7 ++++--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs b/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs index 9b8a1e9c98..6a263cb6ae 100644 --- a/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs +++ b/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs @@ -47,9 +47,6 @@ namespace Umbraco.Cms.Tests.Integration.Cache new EventDefinition>(null, UserService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, UserService, new DeleteEventArgs(Enumerable.Empty())), - new EventDefinition>(null, LocalizationService, new SaveEventArgs(Enumerable.Empty())), - new EventDefinition>(null, LocalizationService, new DeleteEventArgs(Enumerable.Empty())), - new EventDefinition>(null, DataTypeService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, DataTypeService, new DeleteEventArgs(Enumerable.Empty())), @@ -59,9 +56,6 @@ namespace Umbraco.Cms.Tests.Integration.Cache new EventDefinition>(null, DomainService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, DomainService, new DeleteEventArgs(Enumerable.Empty())), - new EventDefinition>(null, LocalizationService, new SaveEventArgs(Enumerable.Empty())), - new EventDefinition>(null, LocalizationService, new DeleteEventArgs(Enumerable.Empty())), - new EventDefinition>(null, ContentTypeService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, ContentTypeService, new DeleteEventArgs(Enumerable.Empty())), new EventDefinition>(null, MediaTypeService, new SaveEventArgs(Enumerable.Empty())), diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs index fcfd81a39d..61334940e1 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs @@ -256,15 +256,16 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping }; service.Save(item); + // Refresh the cache manually because we can't unbind + service.GetDictionaryItemById(item.Id); + service.GetLanguageById(lang.Id); + // global cache contains the entity var globalCached = (IDictionaryItem)globalCache.Get(GetCacheIdKey(item.Id), () => null); Assert.IsNotNull(globalCached); Assert.AreEqual(item.Id, globalCached.Id); Assert.AreEqual("item-key", globalCached.ItemKey); - // _distributedCacheBinder = new DistributedCacheBinder(new DistributedCache(ServerMessenger, CacheRefresherCollection), GetRequiredService(), GetRequiredService>()); - // _distributedCacheBinder.BindEvents(true); - Assert.IsNull(scopeProvider.AmbientScope); using (IScope scope = scopeProvider.CreateScope(repositoryCacheMode: RepositoryCacheMode.Scoped)) { From fbcaa6adea8a2f006a5b638ae55507485732c459 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 25 Mar 2021 16:17:46 +0100 Subject: [PATCH 48/78] https://github.com/umbraco/Umbraco-CMS/issues/10060 - Fixed issue where appsettings section for modelsbuilder was not used. --- .../DependencyInjection/UmbracoBuilder.Configuration.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index a9e112efc4..bd834a5427 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -30,7 +30,8 @@ namespace Umbraco.Cms.Core.DependencyInjection builder.Services.AddSingleton, UnattendedSettingsValidator>(); // Register configuration sections. - builder.Services.Configure(builder.Config.GetSection(Constants.Configuration.ConfigModelsBuilder), o => o.BindNonPublicProperties = true); + + builder.Services.Configure(builder.Config.GetSection(Constants.Configuration.ConfigModelsBuilder), o => o.BindNonPublicProperties = true); builder.Services.Configure(builder.Config.GetSection("ConnectionStrings"), o => o.BindNonPublicProperties = true); AddOptions(builder, Constants.Configuration.ConfigActiveDirectory); From 33b5f402b433f777d1c5e9617505969ccb45edb1 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 26 Mar 2021 14:06:17 +1100 Subject: [PATCH 49/78] reverts MemberGroupController and removes TODOs --- .../Controllers/MemberController.cs | 5 - .../Controllers/MemberGroupController.cs | 117 +++--------------- 2 files changed, 19 insertions(+), 103 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index 05e4cc7486..567303e150 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -194,7 +194,6 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [OutgoingEditorModelEvent] public MemberDisplay GetByKey(Guid key) { - //TODO: convert to identity IMember foundMember = _memberService.GetByKey(key); if (foundMember == null) { @@ -395,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; @@ -655,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 f90bd6458c..88825c0d9a 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Mapping; @@ -28,44 +26,30 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private readonly IMemberGroupService _memberGroupService; private readonly UmbracoMapper _umbracoMapper; private readonly ILocalizedTextService _localizedTextService; - private readonly RoleManager _roleManager; public MemberGroupController( IMemberGroupService memberGroupService, UmbracoMapper umbracoMapper, - ILocalizedTextService localizedTextService, - RoleManager roleManager - ) + ILocalizedTextService localizedTextService) { _memberGroupService = memberGroupService ?? throw new ArgumentNullException(nameof(memberGroupService)); - _roleManager = roleManager ?? throw new ArgumentNullException(nameof(roleManager)); _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); } - //TODO: are there any repercussions elsewhere for us changing these to async? - /// /// Gets the member group json for the member group id /// /// /// - public async Task> GetById(int id) + public ActionResult GetById(int id) { - //TODO: did we envisage this - combination of service and identity manager? - IdentityRole identityRole = await _roleManager.FindByIdAsync(id.ToString()); - if (identityRole == null) - { - return NotFound(); - } - IMemberGroup memberGroup = _memberGroupService.GetById(id); if (memberGroup == null) { return NotFound(); } - //TODO: the default identity role doesn't have all the properties IMemberGroup had, e.g. CreatorId MemberGroupDisplay dto = _umbracoMapper.Map(memberGroup); return dto; } @@ -75,14 +59,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// /// - public async Task> GetById(Guid id) + public ActionResult GetById(Guid id) { - //TODO: did we envisage just identity or a combination of service and identity manager? - //IdentityRole identityRole = await _roleManager.FindByIdAsync(id.ToString()); - //if (identityRole == null) - //{ - // return NotFound(); - //} IMemberGroup memberGroup = _memberGroupService.GetById(id); if (memberGroup == null) { @@ -97,7 +75,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// /// - public async Task> GetById(Udi id) + public ActionResult GetById(Udi id) { var guidUdi = id as GuidUdi; if (guidUdi == null) @@ -105,13 +83,6 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return NotFound(); } - //TODO: can we do this via identity? - IdentityRole identityRole = await _roleManager.FindByIdAsync(id.ToString()); - if (identityRole == null) - { - return NotFound(); - } - IMemberGroup memberGroup = _memberGroupService.GetById(guidUdi.Guid); if (memberGroup == null) { @@ -122,47 +93,25 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } public IEnumerable GetByIds([FromQuery] int[] ids) - { - //var roles = new List(); - - //foreach (int id in ids) - //{ - // IdentityRole role = await _roleManager.FindByIdAsync(id.ToString()); - // roles.Add(role); - //} - - //return roles.Select(x => _umbracoMapper.Map(x)); - //TODO: does this need to be done via identity? - - return _memberGroupService.GetByIds(ids) - .Select(_umbracoMapper.Map); - } + => _memberGroupService.GetByIds(ids).Select(_umbracoMapper.Map); [HttpDelete] [HttpPost] - public async Task DeleteById(int id) + public IActionResult DeleteById(int id) { - IdentityRole role = await _roleManager.FindByIdAsync(id.ToString()); - - if (role == null) + var memberGroup = _memberGroupService.GetById(id); + if (memberGroup == null) { return NotFound(); } - IdentityResult roleDeleted = await _roleManager.DeleteAsync(role); - if (roleDeleted.Succeeded) - { - return Ok(); - } - else - { - return Problem("Issue during deletion - please see logs"); - } + _memberGroupService.Delete(memberGroup); + return Ok(); } - //TODO: we don't currently implement IQueryableRoleStore, still using original service - //public IEnumerable GetAllGroups() => _roleManager.Roles.Select(x => _umbracoMapper.Map(x)); - public IEnumerable GetAllGroups() => _memberGroupService.GetAll().Select(x => _umbracoMapper.Map(x)); + public IEnumerable GetAllGroups() + => _memberGroupService.GetAll() + .Select(_umbracoMapper.Map); public MemberGroupDisplay GetEmpty() { @@ -170,47 +119,19 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return _umbracoMapper.Map(item); } - /// - /// Saves the member group via the identity role - /// If new, creates a new role, else updates the existing role - /// - /// - /// - public async Task> PostSave(MemberGroupSave saveModel) + public ActionResult PostSave(MemberGroupSave saveModel) { - int id = int.Parse(saveModel.Id.ToString()); - - IdentityRole role; - IdentityResult updatedResult; - if (id > 0) - { - role = await _roleManager.FindByIdAsync(saveModel.Id.ToString()); - role.Name = saveModel.Name; - updatedResult = await _roleManager.UpdateAsync(role); - - } - else - { - role = new IdentityRole(saveModel.Name); - updatedResult = await _roleManager.CreateAsync(role); - } - - if (!updatedResult.Succeeded) - { - //TODO: what to return if there is a failed identity result - return Problem(); - } - - //TODO: do we need to refetch the member group? - int roleId = int.Parse(role.Id); - IMemberGroup memberGroup = _memberGroupService.GetById(roleId); + var id = int.Parse(saveModel.Id.ToString()); + IMemberGroup memberGroup = id > 0 ? _memberGroupService.GetById(id) : new MemberGroup(); if (memberGroup == null) { return NotFound(); } - //TODO: should we return the identity role or return the group from the service? + memberGroup.Name = saveModel.Name; + _memberGroupService.Save(memberGroup); + MemberGroupDisplay display = _umbracoMapper.Map(memberGroup); display.AddSuccessNotification( From db3dc01321ccba0977cc33b3f39406ad263863ea Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 26 Mar 2021 14:18:41 +1100 Subject: [PATCH 50/78] removes todos, reverts tree controller, fixes up review notes. --- .../Mapping/MemberTabsAndPropertiesMapper.cs | 15 +++++++++------ .../Security/MemberUserStore.cs | 14 ++++++-------- .../Mapping/MemberMapDefinition.cs | 14 -------------- .../Trees/MemberGroupTreeController.cs | 14 ++++---------- 4 files changed, 19 insertions(+), 38 deletions(-) diff --git a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs index 02ee4fe744..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,10 +233,9 @@ 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" - //TODO: use member role manager instead var result = _memberGroupService.GetAll() .Select(x => x.Name) // if a role starts with __umbracoRole we won't show it as it's an internal role used for public access @@ -246,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.Infrastructure/Security/MemberUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs index 2f2127201d..c0b9a19ef1 100644 --- a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs @@ -363,13 +363,6 @@ namespace Umbraco.Cms.Core.Security cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - MemberIdentityUser user = await FindUserAsync(userId, cancellationToken); - if (user == null) - { - //TODO: error throw or null result? - return await Task.FromResult((IdentityUserLogin)null); - } - if (string.IsNullOrWhiteSpace(loginProvider)) { throw new ArgumentNullException(nameof(loginProvider)); @@ -380,11 +373,16 @@ namespace Umbraco.Cms.Core.Security throw new ArgumentNullException(nameof(providerKey)); } + MemberIdentityUser user = await FindUserAsync(userId, cancellationToken); + if (user == 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) { - //TODO: error throw or null result? return await Task.FromResult((IdentityUserLogin)null); } diff --git a/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs index 7ee456a323..be713ddfa9 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs @@ -35,7 +35,6 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping mapper.Define((source, context) => new MemberDisplay(), Map); mapper.Define((source, context) => new MemberBasic(), Map); mapper.Define((source, context) => new MemberGroupDisplay(), Map); - mapper.Define((source, context) => new MemberGroupDisplay(), Map); mapper.Define((source, context) => new ContentPropertyCollectionDto(), Map); } @@ -103,18 +102,5 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping { target.Properties = context.MapEnumerable(source.Properties); } - - /// - /// Maps an identity role to a member group display - /// - /// - /// - /// - private void Map(IdentityRole source, MemberGroupDisplay target, MapperContext context) - { - //TODO: this is all that is mapped at this time, we're losing a lot of properties - target.Id = source.Id; - target.Name = source.Name; - } } } diff --git a/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs index 5888068656..0559a17a53 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs @@ -28,29 +28,23 @@ namespace Umbraco.Cms.Web.BackOffice.Trees IMemberGroupService memberGroupService, IEventAggregator eventAggregator) : base(localizedTextService, umbracoApiControllerTypeCollection, menuItemCollectionFactory, eventAggregator) - { - _memberGroupService = memberGroupService; - } + => _memberGroupService = memberGroupService; - //TODO: change to role store 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 - //TODO: change to role store root.HasChildren = _memberGroupService.GetAll().Any(); return root; } From 48a2a0a357e213e2b44b73cefcf75970a78a8ae6 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 26 Mar 2021 15:54:45 +1100 Subject: [PATCH 51/78] remove httpcontext requirement from mapper --- .../Controllers/MemberControllerUnitTests.cs | 3 +-- src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs | 5 +---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs index 857f51dc20..321f34f517 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs @@ -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/Mapping/MemberMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs index be713ddfa9..b5592b08ff 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs @@ -19,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) From 29b1aa1b33976c23193c23e6baef74de1e64c182 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 26 Mar 2021 16:43:49 +1100 Subject: [PATCH 52/78] Refactor MemberRoleStore to not be generic and use our own UmbracoIdentityRole which has support for change tracking, uses builders for models in test --- .../Security/MemberRoleStore.cs | 75 +++----- .../Security/UmbracoIdentityRole.cs | 97 ++++++++++ .../Security/UmbracoIdentityUser.cs | 1 + .../Builders/Extensions/BuilderExtensions.cs | 7 + .../Builders/Interfaces/IWithIdBuilder.cs | 5 + .../Builders/UmbracoIdentityRoleBuilder.cs | 47 +++++ .../Builders/UserBuilder.cs | 2 + .../Security/MemberRoleStoreTests.cs | 169 ++++++------------ .../ServiceCollectionExtensions.cs | 2 +- 9 files changed, 239 insertions(+), 166 deletions(-) create mode 100644 src/Umbraco.Infrastructure/Security/UmbracoIdentityRole.cs create mode 100644 src/Umbraco.Tests.Common/Builders/UmbracoIdentityRoleBuilder.cs diff --git a/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs b/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs index 8f4bd0cd17..279735bfa2 100644 --- a/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs @@ -3,6 +3,7 @@ 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 @@ -10,7 +11,7 @@ namespace Umbraco.Cms.Core.Security /// /// A custom user store that uses Umbraco member data /// - public class MemberRoleStore : IRoleStore where TRole : IdentityRole + public class MemberRoleStore : IRoleStore { private readonly IMemberGroupService _memberGroupService; private bool _disposed; @@ -33,7 +34,7 @@ namespace Umbraco.Cms.Core.Security public IdentityErrorDescriber ErrorDescriber { get; set; } /// - public Task CreateAsync(TRole role, CancellationToken cancellationToken = default) + public Task CreateAsync(UmbracoIdentityRole role, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -57,7 +58,7 @@ namespace Umbraco.Cms.Core.Security /// - public Task UpdateAsync(TRole role, CancellationToken cancellationToken = default) + public Task UpdateAsync(UmbracoIdentityRole role, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -79,19 +80,17 @@ namespace Umbraco.Cms.Core.Security { _memberGroupService.Save(memberGroup); } - //TODO: if nothing changed, do we need to report this? + return Task.FromResult(IdentityResult.Success); } else { - //TODO: throw exception when not found, or return failure? return Task.FromResult(IdentityResult.Failed(_memberGroupNotFoundError)); } - } /// - public Task DeleteAsync(TRole role, CancellationToken cancellationToken = default) + public Task DeleteAsync(UmbracoIdentityRole role, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -103,8 +102,7 @@ namespace Umbraco.Cms.Core.Security if (!int.TryParse(role.Id, out int roleId)) { - //TODO: what identity error should we return in this case? - return Task.FromResult(IdentityResult.Failed(_intParseError)); + throw new ArgumentException("The Id of the role is not an integer"); } IMemberGroup memberGroup = _memberGroupService.GetById(roleId); @@ -121,8 +119,7 @@ namespace Umbraco.Cms.Core.Security } /// - - public Task GetRoleIdAsync(TRole role, CancellationToken cancellationToken = default) + public Task GetRoleIdAsync(UmbracoIdentityRole role, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -136,7 +133,7 @@ namespace Umbraco.Cms.Core.Security } /// - public Task GetRoleNameAsync(TRole role, CancellationToken cancellationToken = default) + public Task GetRoleNameAsync(UmbracoIdentityRole role, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -150,47 +147,28 @@ namespace Umbraco.Cms.Core.Security } /// - public Task SetRoleNameAsync(TRole role, string roleName, CancellationToken cancellationToken = default) + public Task SetRoleNameAsync(UmbracoIdentityRole role, string roleName, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - if (role == null) { throw new ArgumentNullException(nameof(role)); } - - if (!int.TryParse(role.Id, out int roleId)) - { - //TODO: what identity error should we return in this case? - return Task.FromResult(IdentityResult.Failed(ErrorDescriber.DefaultError())); - } - - IMemberGroup memberGroup = _memberGroupService.GetById(roleId); - - if (memberGroup != null) - { - //TODO: confirm logic - memberGroup.Name = roleName; - _memberGroupService.Save(memberGroup); - role.Name = roleName; - } - else - { - return Task.FromResult(IdentityResult.Failed(_memberGroupNotFoundError)); - } - + role.Name = roleName; return Task.CompletedTask; } /// - public Task GetNormalizedRoleNameAsync(TRole role, CancellationToken cancellationToken = default) => GetRoleNameAsync(role, cancellationToken); + public Task GetNormalizedRoleNameAsync(UmbracoIdentityRole role, CancellationToken cancellationToken = default) + => GetRoleNameAsync(role, cancellationToken); /// - public Task SetNormalizedRoleNameAsync(TRole role, string normalizedName, CancellationToken cancellationToken = default) => SetRoleNameAsync(role, normalizedName, cancellationToken); + public Task SetNormalizedRoleNameAsync(UmbracoIdentityRole role, string normalizedName, CancellationToken cancellationToken = default) + => SetRoleNameAsync(role, normalizedName, cancellationToken); /// - public Task FindByIdAsync(string roleId, CancellationToken cancellationToken = default) + public Task FindByIdAsync(string roleId, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -223,7 +201,7 @@ namespace Umbraco.Cms.Core.Security } /// - public Task FindByNameAsync(string name, CancellationToken cancellationToken = default) + public Task FindByNameAsync(string name, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -241,16 +219,16 @@ namespace Umbraco.Cms.Core.Security /// /// /// - private TRole MapFromMemberGroup(IMemberGroup memberGroup) + private UmbracoIdentityRole MapFromMemberGroup(IMemberGroup memberGroup) { - var result = new IdentityRole + var result = new UmbracoIdentityRole { Id = memberGroup.Id.ToString(), Name = memberGroup.Name - //TODO: Are we interested in NormalizedRoleName? + // TODO: Implement this functionality, requires DB and logic updates + //ConcurrencyStamp }; - - return result as TRole; + return result; } /// @@ -259,12 +237,15 @@ namespace Umbraco.Cms.Core.Security /// /// /// - private bool MapToMemberGroup(TRole role, IMemberGroup memberGroup) + private bool MapToMemberGroup(UmbracoIdentityRole role, IMemberGroup memberGroup) { var anythingChanged = false; - if (!string.IsNullOrEmpty(role.Name) && memberGroup.Name != role.Name) + 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; } @@ -272,8 +253,6 @@ namespace Umbraco.Cms.Core.Security return anythingChanged; } - //TODO: is any dispose action necessary here? - /// /// Dispose the store /// 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.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.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs index a6c534d24d..15f4b7f30d 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs @@ -6,8 +6,11 @@ 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 { @@ -16,20 +19,29 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security { private Mock _mockMemberGroupService; private IdentityErrorDescriber ErrorDescriber => new IdentityErrorDescriber(); + private UmbracoIdentityRoleBuilder _roleBuilder; + private MemberGroupBuilder _groupBuilder; - public MemberRoleStore CreateSut() + public MemberRoleStore CreateSut() { _mockMemberGroupService = new Mock(); - return new MemberRoleStore( + 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(); + MemberRoleStore sut = CreateSut(); var fakeCancellationToken = new CancellationToken(); // act @@ -44,12 +56,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenICreateAMemberRole_AndTheGroupIsPopulatedCorrectly_ThenIShouldGetASuccessResultAsync() { // arrange - MemberRoleStore sut = CreateSut(); - var fakeRole = new IdentityRole - { - Id = "777", - Name = "testname" - }; + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithTestName("777").Build(); var fakeCancellationToken = new CancellationToken() { }; IMemberGroup mockMemberGroup = Mock.Of(m => @@ -72,12 +80,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenIUpdateAMemberRole_AndTheGroupExistsWithTheSameName_ThenIShouldGetASuccessResultAsyncButNoUpdatesMade() { // arrange - MemberRoleStore sut = CreateSut(); - var fakeRole = new IdentityRole - { - Id = "777", - Name = "fakeGroupName" - }; + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithName("fakeGroupName").WithId("777").Build(); var fakeCancellationToken = new CancellationToken() { }; IMemberGroup mockMemberGroup = Mock.Of(m => @@ -101,12 +105,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenIUpdateAMemberRole_AndTheGroupExistsWithADifferentSameName_ThenIShouldGetASuccessResultAsyncWithUpdatesMade() { // arrange - MemberRoleStore sut = CreateSut(); - var fakeRole = new IdentityRole - { - Id = "777", - Name = "fakeGroup777" - }; + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithName("fakeGroup777").WithId("777").Build(); var fakeCancellationToken = new CancellationToken() { }; IMemberGroup mockMemberGroup = Mock.Of(m => @@ -131,12 +131,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenIUpdateAMemberRole_AndTheGroupDoesntExist_ThenIShouldGetAFailureResultAsync() { // arrange - MemberRoleStore sut = CreateSut(); - var fakeRole = new IdentityRole - { - Id = "777", - Name = "testname" - }; + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithTestName("777").Build(); var fakeCancellationToken = new CancellationToken() { }; // act @@ -152,12 +148,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenIUpdateAMemberRole_AndTheIdCannotBeParsedToAnInt_ThenIShouldGetAFailureResultAsync() { // arrange - MemberRoleStore sut = CreateSut(); - var fakeRole = new IdentityRole - { - Id = "7a77", - Name = "testname" - }; + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithTestName("7a77").Build(); var fakeCancellationToken = new CancellationToken() { }; // act @@ -173,7 +165,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public void GivenIUpdateAMemberRole_AndTheRoleIsNull_ThenAnExceptionShouldBeThrown() { // arrange - MemberRoleStore sut = CreateSut(); + MemberRoleStore sut = CreateSut(); var fakeCancellationToken = new CancellationToken() { }; // act @@ -188,12 +180,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenIDeleteAMemberRole_AndItExists_ThenTheMemberGroupShouldBeDeleted_AndIShouldGetASuccessResultAsync() { // arrange - MemberRoleStore sut = CreateSut(); - var fakeRole = new IdentityRole - { - Id = "777", - Name = "testname" - }; + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithTestName("777").Build(); var fakeCancellationToken = new CancellationToken() { }; IMemberGroup mockMemberGroup = Mock.Of(m => @@ -213,15 +201,11 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security } [Test] - public async Task GivenIDeleteAMemberRole_AndTheIdCannotBeParsedToAnInt_ThenTheMemberGroupShouldNotBeDeleted_AndIShouldGetAFailResultAsync() + public async Task GivenIDeleteAMemberRole_AndTheIdCannotBeParsedToAnInt_ThenTheMemberGroupShouldNotBeDeleted_AndIShouldGetAnArgumentException() { // arrange - MemberRoleStore sut = CreateSut(); - var fakeRole = new IdentityRole - { - Id = "7a77", - Name = "testname" - }; + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithTestName("7a77").Build(); var fakeCancellationToken = new CancellationToken() { }; IMemberGroup mockMemberGroup = Mock.Of(m => @@ -229,12 +213,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security // act - IdentityResult identityResult = await sut.DeleteAsync(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(); + Assert.ThrowsAsync(async () => await sut.DeleteAsync(fakeRole, fakeCancellationToken)); } @@ -242,12 +221,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenIDeleteAMemberRole_AndItDoesntExist_ThenTheMemberGroupShouldNotBeDeleted_AndIShouldGetAFailResultAsync() { // arrange - MemberRoleStore sut = CreateSut(); - var fakeRole = new IdentityRole - { - Id = "777", - Name = "testname" - }; + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithTestName("777").Build(); var fakeCancellationToken = new CancellationToken() { }; IMemberGroup mockMemberGroup = Mock.Of(m => @@ -268,20 +243,11 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenIFindAMemberRoleByRoleKey_AndRoleKeyExists_ThenIShouldGetASuccessResultAsync() { // arrange - MemberRoleStore sut = CreateSut(); - var fakeRole = new IdentityRole("fakeGroupName") - { - Id = "777" - }; + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithName("fakeGroupName").WithId("777").Build(); int fakeRoleId = 777; - IMemberGroup fakeMemberGroup = new MemberGroup() - { - Name = "fakeGroupName", - CreatorId = 123, - Id = 777, - Key = Guid.NewGuid() - }; + IMemberGroup fakeMemberGroup = _groupBuilder.WithName("fakeGroupName").WithCreatorId(123).WithId(777).WithKey(Guid.NewGuid()).Build(); _mockMemberGroupService.Setup(x => x.GetById(fakeRoleId)).Returns(fakeMemberGroup); @@ -299,12 +265,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenIFindAMemberRoleByRoleId_AndIdCannotBeParsedToAnIntOrGuid_ThenIShouldGetAFailureResultAsync() { // arrange - MemberRoleStore sut = CreateSut(); - var fakeRole = new IdentityRole - { - Id = "7a77", - Name = "testname" - }; + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithTestName("7a77").Build(); var fakeCancellationToken = new CancellationToken() { }; // act @@ -319,21 +281,12 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenIFindAMemberRoleByRoleId_AndIdCannotBeParsedToAnIntButCanBeToGuid_ThenIShouldGetASuccessResultAsync() { // arrange - MemberRoleStore sut = CreateSut(); - var fakeRole = new IdentityRole("fakeGroupName") - { - Id = "777" - }; + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithName("fakeGroupName").WithId("777").Build(); var fakeRoleGuid = Guid.NewGuid(); - IMemberGroup fakeMemberGroup = new MemberGroup() - { - Name = "fakeGroupName", - CreatorId = 123, - Id = 777, - Key = fakeRoleGuid - }; + IMemberGroup fakeMemberGroup = _groupBuilder.WithName("fakeGroupName").WithCreatorId(123).WithId(777).WithKey(fakeRoleGuid).Build(); _mockMemberGroupService.Setup(x => x.GetById(fakeRoleGuid)).Returns(fakeMemberGroup); @@ -352,21 +305,12 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenIFindAMemberRoleByRoleId_AndIdCannotBeParsedToAGuidButCanBeToInt_ThenIShouldGetASuccessResultAsync() { // arrange - MemberRoleStore sut = CreateSut(); - var fakeRole = new IdentityRole("fakeGroupName") - { - Id = "777" - }; + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithName("fakeGroupName").WithId("777").Build(); var fakeRoleId = 777; - IMemberGroup fakeMemberGroup = new MemberGroup() - { - Name = "fakeGroupName", - CreatorId = 123, - Id = 777, - Key = Guid.NewGuid() - }; + IMemberGroup fakeMemberGroup = _groupBuilder.WithName("fakeGroupName").WithCreatorId(123).WithId(777).WithKey(Guid.NewGuid()).Build(); _mockMemberGroupService.Setup(x => x.GetById(fakeRoleId)).Returns(fakeMemberGroup); @@ -385,11 +329,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public async Task GivenIFindAMemberRoleByRoleName_AndRoleNameExists_ThenIShouldGetASuccessResultAsync() { // arrange - MemberRoleStore sut = CreateSut(); - var fakeRole = new IdentityRole("fakeGroupName") - { - Id = "777" - }; + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithName("fakeGroupName").WithId("777").Build(); IMemberGroup mockMemberGroup = Mock.Of(m => m.Name == "fakeGroupName" && @@ -412,11 +353,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public void GivenIFindAMemberRoleByRoleName_AndTheNameIsNull_ThenIShouldGetAnArgumentException() { // arrange - MemberRoleStore sut = CreateSut(); - var fakeRole = new IdentityRole - { - Id = "777" - }; + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithId("777").Build(); var fakeCancellationToken = new CancellationToken() { }; // act @@ -432,7 +370,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public void GivenIGetAMemberRoleId_AndTheRoleIsNull_ThenIShouldGetAnArgumentException() { // arrange - MemberRoleStore sut = CreateSut(); + MemberRoleStore sut = CreateSut(); var fakeCancellationToken = new CancellationToken() { }; // act @@ -446,11 +384,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public void GivenIGetAMemberRoleId_AndTheRoleIsNotNull_ThenIShouldGetTheMemberRole() { // arrange - MemberRoleStore sut = CreateSut(); - var fakeRole = new IdentityRole("fakeGroupName") - { - Id = "777" - }; + MemberRoleStore sut = CreateSut(); + UmbracoIdentityRole fakeRole = _roleBuilder.WithName("fakeGroupName").WithId("777").Build(); string fakeRoleId = fakeRole.Id; var fakeCancellationToken = new CancellationToken(); diff --git a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs index 1ff044ed3f..5182db4e20 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs @@ -68,7 +68,7 @@ namespace Umbraco.Extensions .AddDefaultTokenProviders() .AddMemberManager() .AddUserStore() - .AddRoleStore>() + .AddRoleStore() .AddRoleValidator>() .AddRoleManager>(); From 005a23958c4196d0fbc2b5c396f86cc0a24b33d0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 26 Mar 2021 16:52:42 +1100 Subject: [PATCH 53/78] remove todo --- .../Persistence/Repositories/Implement/MemberRepository.cs | 1 - 1 file changed, 1 deletion(-) 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()) From 03782bca866460ea71f029007a30546e88c22cd6 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Fri, 26 Mar 2021 10:43:07 +0000 Subject: [PATCH 54/78] New generic Deleting Notification to match the DeletedNotification and Saving/Saved ones --- src/Umbraco.Core/Events/DeletingNotification.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/Umbraco.Core/Events/DeletingNotification.cs diff --git a/src/Umbraco.Core/Events/DeletingNotification.cs b/src/Umbraco.Core/Events/DeletingNotification.cs new file mode 100644 index 0000000000..c4e6046652 --- /dev/null +++ b/src/Umbraco.Core/Events/DeletingNotification.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace Umbraco.Cms.Core.Events +{ + public abstract class DeletingNotification : CancelableEnumerableObjectNotification + { + protected DeletingNotification(T target, EventMessages messages) : base(target, messages) + { + } + + protected DeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + + public IEnumerable DeletedEntities => Target; + } +} From 022c5feaf62f520a24b592b62b647a7221cb0936 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Fri, 26 Mar 2021 10:44:07 +0000 Subject: [PATCH 55/78] Notifications for EntityContainers in DataTypeService aka Folders for DataTypes --- .../EntityContainerDeletedNotification.cs | 11 +++++++ .../EntityContainerDeletingNotification.cs | 11 +++++++ .../EntityContainerRenamedNotification.cs | 11 +++++++ .../EntityContainerSavedNotification.cs | 11 +++++++ .../EntityContainerSavingNotification.cs | 11 +++++++ .../Services/Implement/DataTypeService.cs | 31 +++++++++---------- 6 files changed, 70 insertions(+), 16 deletions(-) create mode 100644 src/Umbraco.Core/Events/EntityContainerDeletedNotification.cs create mode 100644 src/Umbraco.Core/Events/EntityContainerDeletingNotification.cs create mode 100644 src/Umbraco.Core/Events/EntityContainerRenamedNotification.cs create mode 100644 src/Umbraco.Core/Events/EntityContainerSavedNotification.cs create mode 100644 src/Umbraco.Core/Events/EntityContainerSavingNotification.cs diff --git a/src/Umbraco.Core/Events/EntityContainerDeletedNotification.cs b/src/Umbraco.Core/Events/EntityContainerDeletedNotification.cs new file mode 100644 index 0000000000..eca96962be --- /dev/null +++ b/src/Umbraco.Core/Events/EntityContainerDeletedNotification.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Events +{ + public class EntityContainerDeletedNotification : DeletedNotification + { + public EntityContainerDeletedNotification(EntityContainer target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Core/Events/EntityContainerDeletingNotification.cs b/src/Umbraco.Core/Events/EntityContainerDeletingNotification.cs new file mode 100644 index 0000000000..58d0830cf9 --- /dev/null +++ b/src/Umbraco.Core/Events/EntityContainerDeletingNotification.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Events +{ + public class EntityContainerDeletingNotification : DeletingNotification + { + public EntityContainerDeletingNotification(EntityContainer target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Core/Events/EntityContainerRenamedNotification.cs b/src/Umbraco.Core/Events/EntityContainerRenamedNotification.cs new file mode 100644 index 0000000000..3fac1fdd40 --- /dev/null +++ b/src/Umbraco.Core/Events/EntityContainerRenamedNotification.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Events +{ + public class EntityContainerRenamedNotification : SavedNotification + { + public EntityContainerRenamedNotification(EntityContainer target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Core/Events/EntityContainerSavedNotification.cs b/src/Umbraco.Core/Events/EntityContainerSavedNotification.cs new file mode 100644 index 0000000000..108d6649df --- /dev/null +++ b/src/Umbraco.Core/Events/EntityContainerSavedNotification.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Events +{ + public class EntityContainerSavedNotification : SavedNotification + { + public EntityContainerSavedNotification(EntityContainer target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Core/Events/EntityContainerSavingNotification.cs b/src/Umbraco.Core/Events/EntityContainerSavingNotification.cs new file mode 100644 index 0000000000..98a8a34926 --- /dev/null +++ b/src/Umbraco.Core/Events/EntityContainerSavingNotification.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Events +{ + public class EntityContainerSavingNotification : SavingNotification + { + public EntityContainerSavingNotification(EntityContainer target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs b/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs index a2ef8f4544..78bf7f062e 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs @@ -31,13 +31,14 @@ namespace Umbraco.Cms.Core.Services.Implement private readonly ILocalizationService _localizationService; private readonly IShortStringHelper _shortStringHelper; private readonly IJsonSerializer _jsonSerializer; + private readonly IEventAggregator _eventAggregator; public DataTypeService(IScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IDataTypeRepository dataTypeRepository, IDataTypeContainerRepository dataTypeContainerRepository, IAuditRepository auditRepository, IEntityRepository entityRepository, IContentTypeRepository contentTypeRepository, IIOHelper ioHelper, ILocalizedTextService localizedTextService, ILocalizationService localizationService, IShortStringHelper shortStringHelper, - IJsonSerializer jsonSerializer) + IJsonSerializer jsonSerializer, IEventAggregator eventAggregator) : base(provider, loggerFactory, eventMessagesFactory) { _dataTypeRepository = dataTypeRepository; @@ -50,6 +51,7 @@ namespace Umbraco.Cms.Core.Services.Implement _localizationService = localizationService; _shortStringHelper = shortStringHelper; _jsonSerializer = jsonSerializer; + _eventAggregator = eventAggregator; } #region Containers @@ -68,7 +70,8 @@ namespace Umbraco.Cms.Core.Services.Implement CreatorId = userId }; - if (scope.Events.DispatchCancelable(SavingContainer, this, new SaveEventArgs(container, evtMsgs))) + var savingEntityContainerNotification = new EntityContainerSavingNotification(container, evtMsgs); + if (scope.Notifications.PublishCancelable(savingEntityContainerNotification)) { scope.Complete(); return OperationResult.Attempt.Cancel(evtMsgs, container); @@ -77,7 +80,8 @@ namespace Umbraco.Cms.Core.Services.Implement _dataTypeContainerRepository.Save(container); scope.Complete(); - scope.Events.Dispatch(SavedContainer, this, new SaveEventArgs(container, evtMsgs)); + scope.Notifications.Publish(new EntityContainerSavedNotification(container, evtMsgs).WithStateFrom(savingEntityContainerNotification)); + // TODO: Audit trail ? return OperationResult.Attempt.Succeed(evtMsgs, container); @@ -153,7 +157,8 @@ namespace Umbraco.Cms.Core.Services.Implement using (var scope = ScopeProvider.CreateScope()) { - if (scope.Events.DispatchCancelable(SavingContainer, this, new SaveEventArgs(container, evtMsgs))) + var savingEntityContainerNotification = new EntityContainerSavingNotification(container, evtMsgs); + if (scope.Notifications.PublishCancelable(savingEntityContainerNotification)) { scope.Complete(); return OperationResult.Attempt.Cancel(evtMsgs); @@ -161,7 +166,7 @@ namespace Umbraco.Cms.Core.Services.Implement _dataTypeContainerRepository.Save(container); - scope.Events.Dispatch(SavedContainer, this, new SaveEventArgs(container, evtMsgs)); + scope.Notifications.Publish(new EntityContainerSavedNotification(container, evtMsgs).WithStateFrom(savingEntityContainerNotification)); scope.Complete(); } @@ -186,7 +191,8 @@ namespace Umbraco.Cms.Core.Services.Implement return Attempt.Fail(new OperationResult(OperationResultType.FailedCannot, evtMsgs)); } - if (scope.Events.DispatchCancelable(DeletingContainer, this, new DeleteEventArgs(container, evtMsgs))) + var deletingEntityContainerNotification = new EntityContainerDeletingNotification(container, evtMsgs); + if (scope.Notifications.PublishCancelable(deletingEntityContainerNotification)) { scope.Complete(); return Attempt.Fail(new OperationResult(OperationResultType.FailedCancelledByEvent, evtMsgs)); @@ -194,7 +200,7 @@ namespace Umbraco.Cms.Core.Services.Implement _dataTypeContainerRepository.Delete(container); - scope.Events.Dispatch(DeletedContainer, this, new DeleteEventArgs(container, evtMsgs)); + scope.Notifications.Publish(new EntityContainerDeletedNotification(container, evtMsgs).WithStateFrom(deletingEntityContainerNotification)); scope.Complete(); } @@ -220,8 +226,7 @@ namespace Umbraco.Cms.Core.Services.Implement _dataTypeContainerRepository.Save(container); scope.Complete(); - // TODO: triggering SavedContainer with a different name?! - scope.Events.Dispatch(SavedContainer, this, new SaveEventArgs(container, evtMsgs), "RenamedContainer"); + scope.Notifications.Publish(new EntityContainerRenamedNotification(container, evtMsgs)); return OperationResult.Attempt.Succeed(OperationResultType.Success, evtMsgs, container); } @@ -529,21 +534,15 @@ namespace Umbraco.Cms.Core.Services.Implement #region Event Handlers - public static event TypedEventHandler> SavingContainer; - public static event TypedEventHandler> SavedContainer; - public static event TypedEventHandler> DeletingContainer; - public static event TypedEventHandler> DeletedContainer; - /// /// Occurs before Delete /// - public static event TypedEventHandler> Deleting; + //public static event TypedEventHandler> Deleting; /// /// Occurs after Delete /// public static event TypedEventHandler> Deleted; - /// /// Occurs before Save /// From e78b8776199a9b560d6fa91d6c899d302dfac265 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Fri, 26 Mar 2021 12:18:10 +0000 Subject: [PATCH 56/78] Remove reference to old events --- .../Cache/DistributedCacheBinder_Handlers.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs index 1aa4906029..d4dfad145e 100644 --- a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs +++ b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs @@ -67,12 +67,6 @@ namespace Umbraco.Cms.Core.Cache Bind(() => LocalizationService.SavedDictionaryItem += LocalizationService_SavedDictionaryItem, () => LocalizationService.SavedDictionaryItem -= LocalizationService_SavedDictionaryItem); - // bind to data type events - Bind(() => DataTypeService.Deleted += DataTypeService_Deleted, - () => DataTypeService.Deleted -= DataTypeService_Deleted); - Bind(() => DataTypeService.Saved += DataTypeService_Saved, - () => DataTypeService.Saved -= DataTypeService_Saved); - // bind to stylesheet events Bind(() => FileService.SavedStylesheet += FileService_SavedStylesheet, () => FileService.SavedStylesheet -= FileService_SavedStylesheet); From 3cd2d9722632d7eda87024b76513b641c66629f9 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Fri, 26 Mar 2021 12:23:27 +0000 Subject: [PATCH 57/78] Move last events to notifications --- .../Events/DataTypeDeletedNotification.cs | 11 +++ .../Events/DataTypeDeletingNotification.cs | 11 +++ .../Events/DataTypeMovedNotification.cs | 11 +++ .../Events/DataTypeMovingNotification.cs | 11 +++ .../Events/DataTypeSavedNotification.cs | 16 +++++ .../Events/DataTypeSavingNotification.cs | 16 +++++ .../Services/Implement/DataTypeService.cs | 69 ++++++------------- 7 files changed, 98 insertions(+), 47 deletions(-) create mode 100644 src/Umbraco.Core/Events/DataTypeDeletedNotification.cs create mode 100644 src/Umbraco.Core/Events/DataTypeDeletingNotification.cs create mode 100644 src/Umbraco.Core/Events/DataTypeMovedNotification.cs create mode 100644 src/Umbraco.Core/Events/DataTypeMovingNotification.cs create mode 100644 src/Umbraco.Core/Events/DataTypeSavedNotification.cs create mode 100644 src/Umbraco.Core/Events/DataTypeSavingNotification.cs diff --git a/src/Umbraco.Core/Events/DataTypeDeletedNotification.cs b/src/Umbraco.Core/Events/DataTypeDeletedNotification.cs new file mode 100644 index 0000000000..b3461e27f1 --- /dev/null +++ b/src/Umbraco.Core/Events/DataTypeDeletedNotification.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Events +{ + public class DataTypeDeletedNotification : DeletedNotification + { + public DataTypeDeletedNotification(IDataType target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Core/Events/DataTypeDeletingNotification.cs b/src/Umbraco.Core/Events/DataTypeDeletingNotification.cs new file mode 100644 index 0000000000..16b2ee68ac --- /dev/null +++ b/src/Umbraco.Core/Events/DataTypeDeletingNotification.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Events +{ + public class DataTypeDeletingNotification : DeletingNotification + { + public DataTypeDeletingNotification(IDataType target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Core/Events/DataTypeMovedNotification.cs b/src/Umbraco.Core/Events/DataTypeMovedNotification.cs new file mode 100644 index 0000000000..10e42c7a82 --- /dev/null +++ b/src/Umbraco.Core/Events/DataTypeMovedNotification.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Events +{ + public class DataTypeMovedNotification : MovedNotification + { + public DataTypeMovedNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Core/Events/DataTypeMovingNotification.cs b/src/Umbraco.Core/Events/DataTypeMovingNotification.cs new file mode 100644 index 0000000000..4b48f51a00 --- /dev/null +++ b/src/Umbraco.Core/Events/DataTypeMovingNotification.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Events +{ + public class DataTypeMovingNotification : MovingNotification + { + public DataTypeMovingNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Core/Events/DataTypeSavedNotification.cs b/src/Umbraco.Core/Events/DataTypeSavedNotification.cs new file mode 100644 index 0000000000..09c42d32a9 --- /dev/null +++ b/src/Umbraco.Core/Events/DataTypeSavedNotification.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Events +{ + public class DataTypeSavedNotification : SavedNotification + { + public DataTypeSavedNotification(IDataType target, EventMessages messages) : base(target, messages) + { + } + + public DataTypeSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Core/Events/DataTypeSavingNotification.cs b/src/Umbraco.Core/Events/DataTypeSavingNotification.cs new file mode 100644 index 0000000000..dd110df5a3 --- /dev/null +++ b/src/Umbraco.Core/Events/DataTypeSavingNotification.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Events +{ + public class DataTypeSavingNotification : SavingNotification + { + public DataTypeSavingNotification(IDataType target, EventMessages messages) : base(target, messages) + { + } + + public DataTypeSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs b/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs index 78bf7f062e..b96f54dc14 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs @@ -346,8 +346,10 @@ namespace Umbraco.Cms.Core.Services.Implement using (var scope = ScopeProvider.CreateScope()) { var moveEventInfo = new MoveEventInfo(toMove, toMove.Path, parentId); - var moveEventArgs = new MoveEventArgs(evtMsgs, moveEventInfo); - if (scope.Events.DispatchCancelable(Moving, this, moveEventArgs)) + // var moveEventArgs = new MoveEventArgs(evtMsgs, moveEventInfo); + + var movingDataTypeNotification = new DataTypeMovingNotification(moveEventInfo, evtMsgs); + if (scope.Notifications.PublishCancelable(movingDataTypeNotification)) { scope.Complete(); return OperationResult.Attempt.Fail(MoveOperationStatusType.FailedCancelledByEvent, evtMsgs); @@ -364,9 +366,8 @@ namespace Umbraco.Cms.Core.Services.Implement } moveInfo.AddRange(_dataTypeRepository.Move(toMove, container)); - moveEventArgs.MoveInfoCollection = moveInfo; - moveEventArgs.CanCancel = false; - scope.Events.Dispatch(Moved, this, moveEventArgs); + scope.Notifications.Publish(new DataTypeMovedNotification(moveEventInfo, evtMsgs).WithStateFrom(movingDataTypeNotification)); + scope.Complete(); } catch (DataOperationException ex) @@ -386,12 +387,15 @@ namespace Umbraco.Cms.Core.Services.Implement /// Id of the user issuing the save public void Save(IDataType dataType, int userId = Cms.Core.Constants.Security.SuperUserId) { + var evtMsgs = EventMessagesFactory.Get(); dataType.CreatorId = userId; using (var scope = ScopeProvider.CreateScope()) { var saveEventArgs = new SaveEventArgs(dataType); - if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs)) + + var savingDataTypeNotification = new DataTypeSavingNotification(dataType, evtMsgs); + if (scope.Notifications.PublishCancelable(savingDataTypeNotification)) { scope.Complete(); return; @@ -409,8 +413,8 @@ namespace Umbraco.Cms.Core.Services.Implement _dataTypeRepository.Save(dataType); - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(Saved, this, saveEventArgs); + scope.Notifications.Publish(new DataTypeSavedNotification(dataType, evtMsgs).WithStateFrom(savingDataTypeNotification)); + Audit(AuditType.Save, userId, dataType.Id); scope.Complete(); } @@ -434,12 +438,14 @@ namespace Umbraco.Cms.Core.Services.Implement /// Boolean indicating whether or not to raise events public void Save(IEnumerable dataTypeDefinitions, int userId, bool raiseEvents) { + var evtMsgs = EventMessagesFactory.Get(); var dataTypeDefinitionsA = dataTypeDefinitions.ToArray(); - var saveEventArgs = new SaveEventArgs(dataTypeDefinitionsA); + //var saveEventArgs = new SaveEventArgs(dataTypeDefinitionsA); using (var scope = ScopeProvider.CreateScope()) { - if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs)) + var savingDataTypeNotification = new DataTypeSavingNotification(dataTypeDefinitions, evtMsgs); + if (raiseEvents && scope.Notifications.PublishCancelable(savingDataTypeNotification)) { scope.Complete(); return; @@ -453,8 +459,7 @@ namespace Umbraco.Cms.Core.Services.Implement if (raiseEvents) { - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(Saved, this, saveEventArgs); + scope.Notifications.Publish(new DataTypeSavedNotification(dataTypeDefinitions, evtMsgs).WithStateFrom(savingDataTypeNotification)); } Audit(AuditType.Save, userId, -1); @@ -473,10 +478,11 @@ namespace Umbraco.Cms.Core.Services.Implement /// Optional Id of the user issuing the deletion public void Delete(IDataType dataType, int userId = Cms.Core.Constants.Security.SuperUserId) { + var evtMsgs = EventMessagesFactory.Get(); using (var scope = ScopeProvider.CreateScope()) { - var deleteEventArgs = new DeleteEventArgs(dataType); - if (scope.Events.DispatchCancelable(Deleting, this, deleteEventArgs)) + var deletingDataTypeNotification = new DataTypeDeletingNotification(dataType, evtMsgs); + if (scope.Notifications.PublishCancelable(deletingDataTypeNotification)) { scope.Complete(); return; @@ -511,8 +517,8 @@ namespace Umbraco.Cms.Core.Services.Implement _dataTypeRepository.Delete(dataType); - deleteEventArgs.CanCancel = false; - scope.Events.Dispatch(Deleted, this, deleteEventArgs); + scope.Notifications.Publish(new DataTypeDeletedNotification(dataType, evtMsgs).WithStateFrom(deletingDataTypeNotification)); + Audit(AuditType.Delete, userId, dataType.Id); scope.Complete(); @@ -532,36 +538,5 @@ namespace Umbraco.Cms.Core.Services.Implement _auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetName(UmbracoObjectTypes.DataType))); } - #region Event Handlers - - /// - /// Occurs before Delete - /// - //public static event TypedEventHandler> Deleting; - - /// - /// Occurs after Delete - /// - public static event TypedEventHandler> Deleted; - /// - /// Occurs before Save - /// - public static event TypedEventHandler> Saving; - - /// - /// Occurs after Save - /// - public static event TypedEventHandler> Saved; - - /// - /// Occurs before Move - /// - public static event TypedEventHandler> Moving; - - /// - /// Occurs after Move - /// - public static event TypedEventHandler> Moved; - #endregion } } From 32419835e2fb42824c44ee054eb14eb19d977e5d Mon Sep 17 00:00:00 2001 From: Mole Date: Fri, 26 Mar 2021 14:28:19 +0100 Subject: [PATCH 58/78] Move DeletingNotification and make LocalizationService internal --- .../Events}/DeletingNotification.cs | 3 +-- .../Services/Implement/LocalizationService.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) rename src/{Umbraco.Infrastructure/Services/Notifications => Umbraco.Core/Events}/DeletingNotification.cs (84%) diff --git a/src/Umbraco.Infrastructure/Services/Notifications/DeletingNotification.cs b/src/Umbraco.Core/Events/DeletingNotification.cs similarity index 84% rename from src/Umbraco.Infrastructure/Services/Notifications/DeletingNotification.cs rename to src/Umbraco.Core/Events/DeletingNotification.cs index 2dd8e09c6b..46fc9b9a03 100644 --- a/src/Umbraco.Infrastructure/Services/Notifications/DeletingNotification.cs +++ b/src/Umbraco.Core/Events/DeletingNotification.cs @@ -2,9 +2,8 @@ // See LICENSE for more details. using System.Collections.Generic; -using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Infrastructure.Services.Notifications +namespace Umbraco.Cms.Core.Events { public abstract class DeletingNotification : CancelableEnumerableObjectNotification { diff --git a/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs b/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs index b8c36f1ed9..a15d782db5 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs @@ -14,7 +14,7 @@ namespace Umbraco.Cms.Core.Services.Implement /// /// Represents the Localization Service, which is an easy access to operations involving and /// - public class LocalizationService : RepositoryService, ILocalizationService + internal class LocalizationService : RepositoryService, ILocalizationService { private readonly IDictionaryRepository _dictionaryRepository; private readonly ILanguageRepository _languageRepository; From 3f0539cf83e9a12ae49bc4aa0a1ebdd342d2897a Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 26 Mar 2021 18:47:06 +0100 Subject: [PATCH 59/78] Remove static events from MemberGroupService --- .../Cache/DistributedCacheBinder_Handlers.cs | 28 ++++--- .../Compose/PublicAccessComposer.cs | 13 +++- ...essComponent.cs => PublicAccessHandler.cs} | 29 +++----- .../Implement/MemberGroupRepository.cs | 36 +++++---- .../Services/Implement/MemberGroupService.cs | 74 ++++--------------- .../MemberGroupDeletedNotification.cs | 12 +++ .../MemberGroupDeletingNotification.cs | 17 +++++ .../MemberGroupSavedNotification.cs | 17 +++++ .../MemberGroupSavingNotification.cs | 17 +++++ .../Cache/DistributedCacheBinderTests.cs | 3 - .../Scoping/ScopedRepositoryTests.cs | 6 +- 11 files changed, 138 insertions(+), 114 deletions(-) rename src/Umbraco.Infrastructure/Compose/{PublicAccessComponent.cs => PublicAccessHandler.cs} (57%) create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/MemberGroupDeletedNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/MemberGroupDeletingNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/MemberGroupSavedNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/MemberGroupSavingNotification.cs diff --git a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs index de75ac0905..919b39c22d 100644 --- a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs +++ b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs @@ -23,7 +23,9 @@ namespace Umbraco.Cms.Core.Cache INotificationHandler, INotificationHandler, INotificationHandler, - INotificationHandler + INotificationHandler, + INotificationHandler, + INotificationHandler { private List _unbinders; @@ -109,10 +111,6 @@ namespace Umbraco.Cms.Core.Cache () => MemberService.Saved -= MemberService_Saved); Bind(() => MemberService.Deleted += MemberService_Deleted, () => MemberService.Deleted -= MemberService_Deleted); - Bind(() => MemberGroupService.Saved += MemberGroupService_Saved, - () => MemberGroupService.Saved -= MemberGroupService_Saved); - Bind(() => MemberGroupService.Deleted += MemberGroupService_Deleted, - () => MemberGroupService.Deleted -= MemberGroupService_Deleted); // bind to media events - handles all media changes Bind(() => MediaService.TreeChanged += MediaService_TreeChanged, @@ -404,19 +402,27 @@ namespace Umbraco.Cms.Core.Cache #region MemberGroupService - private void MemberGroupService_Deleted(IMemberGroupService sender, DeleteEventArgs e) + /// + /// Fires when a member group is deleted + /// + /// + public void Handle(MemberGroupDeletedNotification notification) { - foreach (var m in e.DeletedEntities.ToArray()) + foreach (IMemberGroup entity in notification.DeletedEntities) { - _distributedCache.RemoveMemberGroupCache(m.Id); + _distributedCache.RemoveMemberGroupCache(entity.Id); } } - private void MemberGroupService_Saved(IMemberGroupService sender, SaveEventArgs e) + /// + /// Fires when a member group is saved + /// + /// + public void Handle(MemberGroupSavedNotification notification) { - foreach (var m in e.SavedEntities.ToArray()) + foreach (IMemberGroup entity in notification.SavedEntities) { - _distributedCache.RemoveMemberGroupCache(m.Id); + _distributedCache.RemoveMemberGroupCache(entity.Id); } } diff --git a/src/Umbraco.Infrastructure/Compose/PublicAccessComposer.cs b/src/Umbraco.Infrastructure/Compose/PublicAccessComposer.cs index 71e48c44d1..c4dcfeb041 100644 --- a/src/Umbraco.Infrastructure/Compose/PublicAccessComposer.cs +++ b/src/Umbraco.Infrastructure/Compose/PublicAccessComposer.cs @@ -1,10 +1,17 @@ -using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Infrastructure.Services.Notifications; namespace Umbraco.Cms.Core.Compose { /// /// Used to ensure that the public access data file is kept up to date properly /// - public sealed class PublicAccessComposer : ComponentComposer, ICoreComposer - { } + public sealed class PublicAccessComposer : ICoreComposer + { + public void Compose(IUmbracoBuilder builder) => + builder + .AddNotificationHandler() + .AddNotificationHandler(); + } } diff --git a/src/Umbraco.Infrastructure/Compose/PublicAccessComponent.cs b/src/Umbraco.Infrastructure/Compose/PublicAccessHandler.cs similarity index 57% rename from src/Umbraco.Infrastructure/Compose/PublicAccessComponent.cs rename to src/Umbraco.Infrastructure/Compose/PublicAccessHandler.cs index b8c60c9d8a..b9ded9aff8 100644 --- a/src/Umbraco.Infrastructure/Compose/PublicAccessComponent.cs +++ b/src/Umbraco.Infrastructure/Compose/PublicAccessHandler.cs @@ -1,34 +1,29 @@ -using System; -using Umbraco.Cms.Core.Composing; +using System; +using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Services.Implement; +using Umbraco.Cms.Infrastructure.Services.Notifications; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Compose { - public sealed class PublicAccessComponent : IComponent + public sealed class PublicAccessHandler : + INotificationHandler, + INotificationHandler { private readonly IPublicAccessService _publicAccessService; - public PublicAccessComponent(IPublicAccessService publicAccessService) - { + + public PublicAccessHandler(IPublicAccessService publicAccessService) => _publicAccessService = publicAccessService ?? throw new ArgumentNullException(nameof(publicAccessService)); - } - public void Initialize() - { - MemberGroupService.Saved += MemberGroupService_Saved; - } + public void Handle(MemberGroupSavedNotification notification) => Handle(notification.SavedEntities); - public void Terminate() - { - MemberGroupService.Saved -= MemberGroupService_Saved; - } + public void Handle(MemberGroupDeletedNotification notification) => Handle(notification.DeletedEntities); - private void MemberGroupService_Saved(IMemberGroupService sender, SaveEventArgs e) + private void Handle(IEnumerable affectedEntities) { - foreach (var grp in e.SavedEntities) + foreach (var grp in affectedEntities) { //check if the name has changed if (grp.AdditionalData.ContainsKey("previousName") diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberGroupRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberGroupRepository.cs index abce17a331..2d73413563 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberGroupRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberGroupRepository.cs @@ -12,15 +12,18 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Services.Notifications; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { internal class MemberGroupRepository : EntityRepositoryBase, IMemberGroupRepository { - public MemberGroupRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { } + private readonly IEventMessagesFactory _eventMessagesFactory; + + public MemberGroupRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IEventMessagesFactory eventMessagesFactory) + : base(scopeAccessor, cache, logger) => + _eventMessagesFactory = eventMessagesFactory; protected override IMemberGroup PerformGet(int id) { @@ -156,10 +159,14 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement }; PersistNewItem(grp); - if (AmbientScope.Events.DispatchCancelable(SavingMemberGroup, this, new SaveEventArgs(grp))) + var evtMsgs = _eventMessagesFactory.Get(); + if (AmbientScope.Notifications.PublishCancelable(new MemberGroupSavingNotification(grp, evtMsgs))) + { return null; + } + + AmbientScope.Notifications.Publish(new MemberGroupSavedNotification(grp, evtMsgs)); - AmbientScope.Events.Dispatch(SavedMemberGroup, this, new SaveEventArgs(grp)); return grp; } @@ -240,13 +247,16 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement var missingRoles = roleNames.Except(existingRoles, StringComparer.CurrentCultureIgnoreCase); var missingGroups = missingRoles.Select(x => new MemberGroup {Name = x}).ToArray(); - if (AmbientScope.Events.DispatchCancelable(SavingMemberGroup, this, new SaveEventArgs(missingGroups))) + var evtMsgs = _eventMessagesFactory.Get(); + if (AmbientScope.Notifications.PublishCancelable(new MemberGroupSavingNotification(missingGroups, evtMsgs))) + { return; + } foreach (var m in missingGroups) PersistNewItem(m); - AmbientScope.Events.Dispatch(SavedMemberGroup, this, new SaveEventArgs(missingGroups)); + AmbientScope.Notifications.Publish(new MemberGroupSavedNotification(missingGroups, evtMsgs)); //now go get all the dto's for roles with these role names var rolesForNames = Database.Fetch(existingSql).ToArray(); @@ -310,17 +320,5 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement [Column("MemberGroup")] public int MemberGroupId { get; set; } } - - // TODO: understand why we need these two repository-level events, move them back to service - - /// - /// Occurs before Save - /// - internal static event TypedEventHandler> SavingMemberGroup; - - /// - /// Occurs after Save - /// - internal static event TypedEventHandler> SavedMemberGroup; } } diff --git a/src/Umbraco.Infrastructure/Services/Implement/MemberGroupService.cs b/src/Umbraco.Infrastructure/Services/Implement/MemberGroupService.cs index 5e6138980a..ac3366a065 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; @@ -6,7 +6,7 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; -using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Services.Notifications; namespace Umbraco.Cms.Core.Services.Implement { @@ -16,30 +16,8 @@ namespace Umbraco.Cms.Core.Services.Implement public MemberGroupService(IScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IMemberGroupRepository memberGroupRepository) - : base(provider, loggerFactory, eventMessagesFactory) - { + : base(provider, loggerFactory, eventMessagesFactory) => _memberGroupRepository = memberGroupRepository; - //Proxy events! - MemberGroupRepository.SavedMemberGroup += MemberGroupRepository_SavedMemberGroup; - MemberGroupRepository.SavingMemberGroup += MemberGroupRepository_SavingMemberGroup; - } - - #region Proxy event handlers - - void MemberGroupRepository_SavingMemberGroup(IMemberGroupRepository sender, SaveEventArgs e) - { - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(e.SavedEntities), this)) - e.Cancel = true; - } - - void MemberGroupRepository_SavedMemberGroup(IMemberGroupRepository sender, SaveEventArgs e) - { - // same as above! - - Saved.RaiseEvent(new SaveEventArgs(e.SavedEntities, false), this); - } - - #endregion public IEnumerable GetAll() { @@ -92,10 +70,13 @@ namespace Umbraco.Cms.Core.Services.Implement { throw new InvalidOperationException("The name of a MemberGroup can not be empty"); } + + var evtMsgs = EventMessagesFactory.Get(); + using (var scope = ScopeProvider.CreateScope()) { - var saveEventArgs = new SaveEventArgs(memberGroup); - if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs)) + var savingNotification = new MemberGroupSavingNotification(memberGroup, evtMsgs); + if (raiseEvents && scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); return; @@ -106,18 +87,19 @@ namespace Umbraco.Cms.Core.Services.Implement if (raiseEvents) { - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(Saved, this, saveEventArgs); + scope.Notifications.Publish(new MemberGroupSavedNotification(memberGroup, evtMsgs).WithStateFrom(savingNotification)); } } } public void Delete(IMemberGroup memberGroup) { + var evtMsgs = EventMessagesFactory.Get(); + using (var scope = ScopeProvider.CreateScope()) { - var deleteEventArgs = new DeleteEventArgs(memberGroup); - if (scope.Events.DispatchCancelable(Deleting, this, deleteEventArgs)) + var deletingNotification = new MemberGroupDeletingNotification(memberGroup, evtMsgs); + if (scope.Notifications.PublishCancelable(deletingNotification)) { scope.Complete(); return; @@ -125,35 +107,9 @@ namespace Umbraco.Cms.Core.Services.Implement _memberGroupRepository.Delete(memberGroup); scope.Complete(); - deleteEventArgs.CanCancel = false; - scope.Events.Dispatch(Deleted, this, deleteEventArgs); + + scope.Notifications.Publish(new MemberGroupDeletedNotification(memberGroup, evtMsgs).WithStateFrom(deletingNotification)); } } - - /// - /// Occurs before Delete of a member group - /// - public static event TypedEventHandler> Deleting; - - /// - /// Occurs after Delete of a member group - /// - public static event TypedEventHandler> Deleted; - - /// - /// Occurs before Save of a member group - /// - /// - /// We need to proxy these events because the events need to take place at the repo level - /// - public static event TypedEventHandler> Saving; - - /// - /// Occurs after Save of a member group - /// - /// - /// We need to proxy these events because the events need to take place at the repo level - /// - public static event TypedEventHandler> Saved; } } diff --git a/src/Umbraco.Infrastructure/Services/Notifications/MemberGroupDeletedNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/MemberGroupDeletedNotification.cs new file mode 100644 index 0000000000..1441344ec5 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/MemberGroupDeletedNotification.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class MemberGroupDeletedNotification : DeletedNotification + { + public MemberGroupDeletedNotification(IMemberGroup target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/MemberGroupDeletingNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/MemberGroupDeletingNotification.cs new file mode 100644 index 0000000000..34e4140752 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/MemberGroupDeletingNotification.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class MemberGroupDeletingNotification : DeletingNotification + { + public MemberGroupDeletingNotification(IMemberGroup target, EventMessages messages) : base(target, messages) + { + } + + public MemberGroupDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/MemberGroupSavedNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/MemberGroupSavedNotification.cs new file mode 100644 index 0000000000..f9af9806ef --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/MemberGroupSavedNotification.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class MemberGroupSavedNotification : SavedNotification + { + public MemberGroupSavedNotification(IMemberGroup target, EventMessages messages) : base(target, messages) + { + } + + public MemberGroupSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/MemberGroupSavingNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/MemberGroupSavingNotification.cs new file mode 100644 index 0000000000..67e03c2268 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/MemberGroupSavingNotification.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class MemberGroupSavingNotification : SavingNotification + { + public MemberGroupSavingNotification(IMemberGroup target, EventMessages messages) : base(target, messages) + { + } + + public MemberGroupSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs b/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs index 6a263cb6ae..1da6261bdc 100644 --- a/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs +++ b/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs @@ -73,9 +73,6 @@ namespace Umbraco.Cms.Tests.Integration.Cache new EventDefinition>(null, MemberService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, MemberService, new DeleteEventArgs(Enumerable.Empty())), - new EventDefinition>(null, MemberGroupService, new SaveEventArgs(Enumerable.Empty())), - new EventDefinition>(null, MemberGroupService, new DeleteEventArgs(Enumerable.Empty())), - // not managed //new EventDefinition>(null, ContentService, new SaveEventArgs(Enumerable.Empty()), "SavedBlueprint"), //new EventDefinition>(null, ContentService, new DeleteEventArgs(Enumerable.Empty()), "DeletedBlueprint"), diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs index 61334940e1..43a3e91b35 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System.Collections.Generic; @@ -55,7 +55,9 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() - .AddNotificationHandler(); + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler(); builder.AddNotificationHandler(); } From eb8a180f1e0838eff2bab1b88b34904fd222d88d Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 29 Mar 2021 16:24:55 +1100 Subject: [PATCH 60/78] Moves base signin manager logic to a base class --- .../Security/MemberSignInManagerTests.cs | 1 - .../Security/BackOfficeSignInManager.cs | 414 +----------------- src/Umbraco.Web.Common/Constants/Security.cs | 14 - .../Security/MemberSignInManager.cs | 207 ++------- 4 files changed, 50 insertions(+), 586 deletions(-) delete mode 100644 src/Umbraco.Web.Common/Constants/Security.cs diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs index 49dc5d9d55..86f96c7d71 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs @@ -24,7 +24,6 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security _mockLogger = new Mock>(); return new MemberSignInManager( _memberManager.Object, - Mock.Of(), Mock.Of(), Mock.Of>(), Mock.Of>(), diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs index 03ebb1aa45..3e921ba0f9 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs @@ -17,17 +17,22 @@ namespace Umbraco.Cms.Web.BackOffice.Security { using Constants = Core.Constants; - public class BackOfficeSignInManager : SignInManager, IBackOfficeSignInManager + /// + /// The sign in manager for back office users + /// + public class BackOfficeSignInManager : UmbracoSignInManager, IBackOfficeSignInManager { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - private const string UmbracoSignInMgrLoginProviderKey = "LoginProvider"; - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - private const string UmbracoSignInMgrXsrfKey = "XsrfId"; - private readonly BackOfficeUserManager _userManager; private readonly IBackOfficeExternalLoginProviders _externalLogins; private readonly GlobalSettings _globalSettings; + protected override string AuthenticationType => Constants.Security.BackOfficeAuthenticationType; + + protected override string ExternalAuthenticationType => Constants.Security.BackOfficeExternalAuthenticationType; + + protected override string TwoFactorAuthenticationType => Constants.Security.BackOfficeTwoFactorAuthenticationType; + + protected override string TwoFactorRememberMeAuthenticationType => Constants.Security.BackOfficeTwoFactorRememberMeAuthenticationType; public BackOfficeSignInManager( BackOfficeUserManager userManager, @@ -46,261 +51,6 @@ namespace Umbraco.Cms.Web.BackOffice.Security _globalSettings = globalSettings.Value; } - // TODO: Have a look into RefreshSignInAsync since we might be able to use this new functionality for auto-cookie renewal in our middleware, though - // i suspect it's taken care of already. - - - /// - public override async Task PasswordSignInAsync(BackOfficeIdentityUser user, string password, bool isPersistent, bool lockoutOnFailure) - { - // override to handle logging/events - var result = await base.PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure); - return await HandleSignIn(user, user.UserName, result); - } - - /// - public override async Task PasswordSignInAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure) - { - // override to handle logging/events - var user = await UserManager.FindByNameAsync(userName); - if (user == null) - return await HandleSignIn(null, userName, SignInResult.Failed); - return await PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure); - } - - /// - public override async Task GetTwoFactorAuthenticationUserAsync() - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - // replaced in order to use a custom auth type - - var info = await RetrieveTwoFactorInfoAsync(); - if (info == null) - { - return null; - } - return await UserManager.FindByIdAsync(info.UserId); - } - - /// - public override async Task TwoFactorSignInAsync(string provider, string code, bool isPersistent, bool rememberClient) - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L552 - // replaced in order to use a custom auth type and to implement logging/events - - var twoFactorInfo = await RetrieveTwoFactorInfoAsync(); - if (twoFactorInfo == null || twoFactorInfo.UserId == null) - { - return SignInResult.Failed; - } - var user = await UserManager.FindByIdAsync(twoFactorInfo.UserId); - if (user == null) - { - return SignInResult.Failed; - } - - var error = await PreSignInCheck(user); - if (error != null) - { - return error; - } - if (await UserManager.VerifyTwoFactorTokenAsync(user, provider, code)) - { - await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent, rememberClient); - return await HandleSignIn(user, user?.UserName, SignInResult.Success); - } - // If the token is incorrect, record the failure which also may cause the user to be locked out - await UserManager.AccessFailedAsync(user); - return await HandleSignIn(user, user?.UserName, SignInResult.Failed); - } - - - /// - public override bool IsSignedIn(ClaimsPrincipal principal) - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L126 - // replaced in order to use a custom auth type - - if (principal == null) - { - throw new ArgumentNullException(nameof(principal)); - } - return principal?.Identities != null && - principal.Identities.Any(i => i.AuthenticationType == Constants.Security.BackOfficeAuthenticationType); - } - - /// - public override async Task RefreshSignInAsync(BackOfficeIdentityUser user) - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L126 - // replaced in order to use a custom auth type - - var auth = await Context.AuthenticateAsync(Constants.Security.BackOfficeAuthenticationType); - IList claims = Array.Empty(); - - var authenticationMethod = auth?.Principal?.FindFirst(ClaimTypes.AuthenticationMethod); - var amr = auth?.Principal?.FindFirst("amr"); - - if (authenticationMethod != null || amr != null) - { - claims = new List(); - if (authenticationMethod != null) - { - claims.Add(authenticationMethod); - } - if (amr != null) - { - claims.Add(amr); - } - } - - await SignInWithClaimsAsync(user, auth?.Properties, claims); - } - - /// - public override async Task SignInWithClaimsAsync(BackOfficeIdentityUser user, AuthenticationProperties authenticationProperties, IEnumerable additionalClaims) - { - // override to replace IdentityConstants.ApplicationScheme with Constants.Security.BackOfficeAuthenticationType - // code taken from aspnetcore: https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - // we also override to set the current HttpContext principal since this isn't done by default - - var userPrincipal = await CreateUserPrincipalAsync(user); - foreach (var claim in additionalClaims) - userPrincipal.Identities.First().AddClaim(claim); - - // FYI (just for informational purposes): - // This calls an ext method will eventually reaches `IAuthenticationService.SignInAsync` - // which then resolves the `IAuthenticationSignInHandler` for the current scheme - // by calling `IAuthenticationHandlerProvider.GetHandlerAsync(context, scheme);` - // which then calls `IAuthenticationSignInHandler.SignInAsync` = CookieAuthenticationHandler.HandleSignInAsync - - // Also note, that when the CookieAuthenticationHandler sign in is successful we handle that event within our - // own ConfigureUmbracoBackOfficeCookieOptions which assigns the current HttpContext.User to the IPrincipal created - - // Also note, this method gets called when performing 2FA logins - - await Context.SignInAsync(Constants.Security.BackOfficeAuthenticationType, - userPrincipal, - authenticationProperties ?? new AuthenticationProperties()); - } - - /// - public override async Task SignOutAsync() - { - // override to replace IdentityConstants.ApplicationScheme with Constants.Security.BackOfficeAuthenticationType - // code taken from aspnetcore: https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - - await Context.SignOutAsync(Constants.Security.BackOfficeAuthenticationType); - await Context.SignOutAsync(Constants.Security.BackOfficeExternalAuthenticationType); - await Context.SignOutAsync(Constants.Security.BackOfficeTwoFactorAuthenticationType); - } - - - /// - public override async Task IsTwoFactorClientRememberedAsync(BackOfficeIdentityUser user) - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 - // to replace the auth scheme - - var userId = await UserManager.GetUserIdAsync(user); - var result = await Context.AuthenticateAsync(Constants.Security.BackOfficeTwoFactorRememberMeAuthenticationType); - return (result?.Principal != null && result.Principal.FindFirstValue(ClaimTypes.Name) == userId); - } - - - /// - public override async Task RememberTwoFactorClientAsync(BackOfficeIdentityUser user) - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 - // to replace the auth scheme - - var principal = await StoreRememberClient(user); - await Context.SignInAsync(Constants.Security.BackOfficeTwoFactorRememberMeAuthenticationType, - principal, - new AuthenticationProperties { IsPersistent = true }); - } - - - /// - public override Task ForgetTwoFactorClientAsync() - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 - // to replace the auth scheme - - return Context.SignOutAsync(Constants.Security.BackOfficeTwoFactorRememberMeAuthenticationType); - } - - - /// - public override async Task TwoFactorRecoveryCodeSignInAsync(string recoveryCode) - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 - // to replace the auth scheme - - var twoFactorInfo = await RetrieveTwoFactorInfoAsync(); - if (twoFactorInfo == null || twoFactorInfo.UserId == null) - { - return SignInResult.Failed; - } - var user = await UserManager.FindByIdAsync(twoFactorInfo.UserId); - if (user == null) - { - return SignInResult.Failed; - } - - var result = await UserManager.RedeemTwoFactorRecoveryCodeAsync(user, recoveryCode); - if (result.Succeeded) - { - await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent: false, rememberClient: false); - return SignInResult.Success; - } - - // We don't protect against brute force attacks since codes are expected to be random. - return SignInResult.Failed; - } - - - /// - public override async Task GetExternalLoginInfoAsync(string expectedXsrf = null) - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 - // to replace the auth scheme - - var auth = await Context.AuthenticateAsync(Constants.Security.BackOfficeExternalAuthenticationType); - var items = auth?.Properties?.Items; - if (auth?.Principal == null || items == null || !items.ContainsKey(UmbracoSignInMgrLoginProviderKey)) - { - return null; - } - - if (expectedXsrf != null) - { - if (!items.ContainsKey(UmbracoSignInMgrXsrfKey)) - { - return null; - } - var userId = items[UmbracoSignInMgrXsrfKey]; - if (userId != expectedXsrf) - { - return null; - } - } - - var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier); - var provider = items[UmbracoSignInMgrLoginProviderKey] as string; - if (providerKey == null || provider == null) - { - return null; - } - - var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName ?? provider; - return new ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName) - { - AuthenticationTokens = auth.Properties.GetTokens(), - AuthenticationProperties = auth.Properties - }; - } - /// /// Custom ExternalLoginSignInAsync overload for handling external sign in with auto-linking /// @@ -367,66 +117,19 @@ namespace Umbraco.Cms.Web.BackOffice.Security return base.GetExternalAuthenticationSchemesAsync(); } - /// - protected override async Task SignInOrTwoFactorAsync(BackOfficeIdentityUser user, bool isPersistent, string loginProvider = null, bool bypassTwoFactor = false) - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - // to replace custom auth types - - if (!bypassTwoFactor && await IsTfaEnabled(user)) - { - if (!await IsTwoFactorClientRememberedAsync(user)) - { - // Store the userId for use after two factor check - var userId = await UserManager.GetUserIdAsync(user); - await Context.SignInAsync(IdentityConstants.TwoFactorUserIdScheme, StoreTwoFactorInfo(userId, loginProvider)); - return SignInResult.TwoFactorRequired; - } - } - // Cleanup external cookie - if (loginProvider != null) - { - await Context.SignOutAsync(Constants.Security.BackOfficeExternalAuthenticationType); - } - if (loginProvider == null) - { - await SignInWithClaimsAsync(user, isPersistent, new Claim[] { new Claim("amr", "pwd") }); - } - else - { - await SignInAsync(user, isPersistent, loginProvider); - } - return SignInResult.Success; - } - /// - /// Called on any login attempt to update the AccessFailedCount and to raise events + /// Overridden to deal with events/notificiations /// /// /// /// /// - private async Task HandleSignIn(BackOfficeIdentityUser user, string username, SignInResult result) + protected override async Task HandleSignIn(BackOfficeIdentityUser user, string username, SignInResult result) { - // TODO: Here I believe we can do all (or most) of the usermanager event raising so that it is not in the AuthenticationController - - if (username.IsNullOrWhiteSpace()) - { - username = "UNKNOWN"; // could happen in 2fa or something else weird - } + result = await base.HandleSignIn(user, username, result); if (result.Succeeded) { - //track the last login date - user.LastLoginDateUtc = DateTime.UtcNow; - if (user.AccessFailedCount > 0) - { - //we have successfully logged in, reset the AccessFailedCount - user.AccessFailedCount = 0; - } - await UserManager.UpdateAsync(user); - - Logger.LogInformation("User: {UserName} logged in from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress); if (user != null) { _userManager.NotifyLoginSuccess(Context.User, user.Id); @@ -435,16 +138,13 @@ namespace Umbraco.Cms.Web.BackOffice.Security else if (result.IsLockedOut) { _userManager.NotifyAccountLocked(Context.User, user.Id); - Logger.LogInformation("Login attempt failed for username {UserName} from IP address {IpAddress}, the user is locked", username, Context.Connection.RemoteIpAddress); } else if (result.RequiresTwoFactor) { _userManager.NotifyLoginRequiresVerification(Context.User, user.Id); - Logger.LogInformation("Login attempt requires verification for username {UserName} from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress); } else if (!result.Succeeded || result.IsNotAllowed) { - Logger.LogInformation("Login attempt failed for username {UserName} from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress); } else { @@ -454,92 +154,6 @@ namespace Umbraco.Cms.Web.BackOffice.Security return result; } - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L782 - // since it's not public - private async Task IsTfaEnabled(BackOfficeIdentityUser user) - => UserManager.SupportsUserTwoFactor && - await UserManager.GetTwoFactorEnabledAsync(user) && - (await UserManager.GetValidTwoFactorProvidersAsync(user)).Count > 0; - - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L743 - // to replace custom auth types - private ClaimsPrincipal StoreTwoFactorInfo(string userId, string loginProvider) - { - var identity = new ClaimsIdentity(Constants.Security.BackOfficeTwoFactorAuthenticationType); - identity.AddClaim(new Claim(ClaimTypes.Name, userId)); - if (loginProvider != null) - { - identity.AddClaim(new Claim(ClaimTypes.AuthenticationMethod, loginProvider)); - } - return new ClaimsPrincipal(identity); - } - - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - // copy is required in order to use custom auth types - private async Task StoreRememberClient(BackOfficeIdentityUser user) - { - var userId = await UserManager.GetUserIdAsync(user); - var rememberBrowserIdentity = new ClaimsIdentity(Constants.Security.BackOfficeTwoFactorRememberMeAuthenticationType); - rememberBrowserIdentity.AddClaim(new Claim(ClaimTypes.Name, userId)); - if (UserManager.SupportsUserSecurityStamp) - { - var stamp = await UserManager.GetSecurityStampAsync(user); - rememberBrowserIdentity.AddClaim(new Claim(Options.ClaimsIdentity.SecurityStampClaimType, stamp)); - } - return new ClaimsPrincipal(rememberBrowserIdentity); - } - - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - // copy is required in order to use custom auth types - private async Task DoTwoFactorSignInAsync(BackOfficeIdentityUser user, TwoFactorAuthenticationInfo twoFactorInfo, bool isPersistent, bool rememberClient) - { - // When token is verified correctly, clear the access failed count used for lockout - await ResetLockout(user); - - var claims = new List - { - new Claim("amr", "mfa") - }; - - // Cleanup external cookie - if (twoFactorInfo.LoginProvider != null) - { - claims.Add(new Claim(ClaimTypes.AuthenticationMethod, twoFactorInfo.LoginProvider)); - await Context.SignOutAsync(Constants.Security.BackOfficeExternalAuthenticationType); - } - // Cleanup two factor user id cookie - await Context.SignOutAsync(Constants.Security.BackOfficeTwoFactorAuthenticationType); - if (rememberClient) - { - await RememberTwoFactorClientAsync(user); - } - await SignInWithClaimsAsync(user, isPersistent, claims); - } - - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - // copy is required in order to use a custom auth type - private async Task RetrieveTwoFactorInfoAsync() - { - var result = await Context.AuthenticateAsync(Constants.Security.BackOfficeTwoFactorAuthenticationType); - if (result?.Principal != null) - { - return new TwoFactorAuthenticationInfo - { - UserId = result.Principal.FindFirstValue(ClaimTypes.Name), - LoginProvider = result.Principal.FindFirstValue(ClaimTypes.AuthenticationMethod) - }; - } - return null; - } - - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L891 - private class TwoFactorAuthenticationInfo - { - public string UserId { get; set; } - public string LoginProvider { get; set; } - } - - /// /// Used for auto linking/creating user accounts for external logins /// diff --git a/src/Umbraco.Web.Common/Constants/Security.cs b/src/Umbraco.Web.Common/Constants/Security.cs deleted file mode 100644 index f2fcbc65f8..0000000000 --- a/src/Umbraco.Web.Common/Constants/Security.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Umbraco.Cms.Web.Common.Constants -{ - public static class Security - { - //TODO: implement 2Factor external - - public const string MemberAuthenticationType = "UmbracoMember"; - //public const string MemberExternalAuthenticationType = "UmbracoMemberExternalCookie"; - //public const string MemberExternalCookieName = "UMB_MEMBEREXTLOGIN"; - public const string MemberTokenAuthenticationType = "UmbracoMemberToken"; - //public const string MemberTwoFactorAuthenticationType = "UmbracoMemberTwoFactorCookie"; - //public const string MemberTwoFactorRememberMeAuthenticationType = "UmbracoMemberTwoFactorRememberMeCookie"; - } -} diff --git a/src/Umbraco.Web.Common/Security/MemberSignInManager.cs b/src/Umbraco.Web.Common/Security/MemberSignInManager.cs index d3adcc4833..392d20ebbe 100644 --- a/src/Umbraco.Web.Common/Security/MemberSignInManager.cs +++ b/src/Umbraco.Web.Common/Security/MemberSignInManager.cs @@ -1,38 +1,42 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Umbraco.Cms.Core.Net; using Umbraco.Cms.Core.Security; -using Umbraco.Extensions; namespace Umbraco.Cms.Web.Common.Security { - //TODO: can any of this be combined/merged with BackOfficeSignInManager using T for the identity user? - //TODO: Need to implement events on member login/logout etc - public class MemberSignInManager : SignInManager + /// + /// The sign in manager for members + /// + public class MemberSignInManager : UmbracoSignInManager { - private const string ClaimType = "amr"; - private const string PasswordValue = "pwd"; - private readonly IIpResolver _ipResolver; - public MemberSignInManager( UserManager memberManager, - IIpResolver ipResolver, IHttpContextAccessor contextAccessor, IUserClaimsPrincipalFactory claimsFactory, IOptions optionsAccessor, ILogger> logger, IAuthenticationSchemeProvider schemes, IUserConfirmation confirmation) : - base(memberManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) => - _ipResolver = ipResolver ?? throw new ArgumentNullException(nameof(ipResolver)); + base(memberManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) + { } + + // use default scheme for members + protected override string AuthenticationType => IdentityConstants.ApplicationScheme; + + // use default scheme for members + protected override string ExternalAuthenticationType => IdentityConstants.ExternalScheme; + + // use default scheme for members + protected override string TwoFactorAuthenticationType => IdentityConstants.TwoFactorUserIdScheme; + + // use default scheme for members + protected override string TwoFactorRememberMeAuthenticationType => IdentityConstants.TwoFactorRememberMeScheme; /// public override async Task PasswordSignInAsync(MemberIdentityUser user, string password, bool isPersistent, bool lockoutOnFailure) @@ -56,179 +60,40 @@ namespace Umbraco.Cms.Web.Common.Security } /// - public override Task GetTwoFactorAuthenticationUserAsync() => throw new NotImplementedException("Two factor is not yet implemented for members"); + public override Task GetTwoFactorAuthenticationUserAsync() + => throw new NotImplementedException("Two factor is not yet implemented for members"); /// - public override Task TwoFactorSignInAsync(string provider, string code, bool isPersistent, bool rememberClient) => throw new NotImplementedException("Two factor is not yet implemented for members"); + public override Task TwoFactorSignInAsync(string provider, string code, bool isPersistent, bool rememberClient) + => throw new NotImplementedException("Two factor is not yet implemented for members"); /// - public override bool IsSignedIn(ClaimsPrincipal principal) - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L126 - // replaced in order to use a custom auth type - // taken from BackOfficeSignInManager - - if (principal == null) - { - throw new ArgumentNullException(nameof(principal)); - } - return principal?.Identities != null && principal.Identities.Any(i => i.AuthenticationType == Constants.Security.MemberAuthenticationType); - } + public override Task IsTwoFactorClientRememberedAsync(MemberIdentityUser user) + => throw new NotImplementedException("Two factor is not yet implemented for members"); /// - public override async Task RefreshSignInAsync(MemberIdentityUser user) - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L126 - // replaced in order to use a custom auth type - - AuthenticateResult auth = await Context.AuthenticateAsync(Constants.Security.MemberAuthenticationType); - IList claims = Array.Empty(); - - Claim authenticationMethod = auth?.Principal?.FindFirst(ClaimTypes.AuthenticationMethod); - Claim amr = auth?.Principal?.FindFirst(ClaimType); - - if (authenticationMethod != null || amr != null) - { - claims = new List(); - if (authenticationMethod != null) - { - claims.Add(authenticationMethod); - } - if (amr != null) - { - claims.Add(amr); - } - } - - await SignInWithClaimsAsync(user, auth?.Properties, claims); - } + public override Task RememberTwoFactorClientAsync(MemberIdentityUser user) + => throw new NotImplementedException("Two factor is not yet implemented for members"); /// - public override async Task SignInWithClaimsAsync(MemberIdentityUser user, AuthenticationProperties authenticationProperties, IEnumerable additionalClaims) - { - // TODO: taken from BackOfficeSigninManager and more notes are there - // override to replace IdentityConstants.ApplicationScheme with Constants.Security.MemberAuthenticationType - // code taken from aspnetcore: https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - // we also override to set the current HttpContext principal since this isn't done by default - - ClaimsPrincipal userPrincipal = await CreateUserPrincipalAsync(user); - foreach (Claim claim in additionalClaims) - { - userPrincipal.Identities.First().AddClaim(claim); - } - - // TODO: For future, this method gets called when performing 2FA logins - await Context.SignInAsync(Constants.Security.MemberAuthenticationType, - userPrincipal, - authenticationProperties ?? new AuthenticationProperties()); - } + public override Task ForgetTwoFactorClientAsync() + => throw new NotImplementedException("Two factor is not yet implemented for members"); /// - public override async Task SignOutAsync() => - //TODO: does members need this custom signout type as per BackOfficeSignInManager? - // override to replace IdentityConstants.ApplicationScheme with Constants.Security.MemberAuthenticationType - // code taken from aspnetcore: https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - await Context.SignOutAsync(Constants.Security.MemberAuthenticationType); + public override Task TwoFactorRecoveryCodeSignInAsync(string recoveryCode) + => throw new NotImplementedException("Two factor is not yet implemented for members"); /// - public override Task IsTwoFactorClientRememberedAsync(MemberIdentityUser user) => throw new NotImplementedException("Two factor is not yet implemented for members"); + public override Task GetExternalLoginInfoAsync(string expectedXsrf = null) + => throw new NotImplementedException("External login is not yet implemented for members"); /// - public override Task RememberTwoFactorClientAsync(MemberIdentityUser user) => throw new NotImplementedException("Two factor is not yet implemented for members"); + public override AuthenticationProperties ConfigureExternalAuthenticationProperties(string provider, string redirectUrl, string userId = null) + => throw new NotImplementedException("External login is not yet implemented for members"); /// - public override Task ForgetTwoFactorClientAsync() => throw new NotImplementedException("Two factor is not yet implemented for members"); + public override Task> GetExternalAuthenticationSchemesAsync() + => throw new NotImplementedException("External login is not yet implemented for members"); - /// - public override Task TwoFactorRecoveryCodeSignInAsync(string recoveryCode) => throw new NotImplementedException("Two factor is not yet implemented for members"); - - /// - public override Task GetExternalLoginInfoAsync(string expectedXsrf = null) => throw new NotImplementedException("External login is not yet implemented for members"); - - /// - public override AuthenticationProperties ConfigureExternalAuthenticationProperties(string provider, string redirectUrl, string userId = null) => throw new NotImplementedException("External login is not yet implemented for members"); - - /// - public override Task> GetExternalAuthenticationSchemesAsync() => throw new NotImplementedException("External login is not yet implemented for members"); - - /// - /// TODO: Two factor is not yet implemented for members - protected override async Task SignInOrTwoFactorAsync(MemberIdentityUser user, bool isPersistent, string loginProvider = null, bool bypassTwoFactor = false) - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - // to replace custom auth types - - //TODO: There is currently no two factor so this needs changing once implemented - if (loginProvider != null) - { - await SignInAsync(user, isPersistent, loginProvider); - } - else - { - await SignInWithClaimsAsync(user, isPersistent, new Claim[] - { - new Claim(ClaimType, PasswordValue) - }); - } - return SignInResult.Success; - } - - /// - /// Called on any login attempt to update the AccessFailedCount and to raise events - /// - /// - /// - /// - /// - private async Task HandleSignIn(MemberIdentityUser user, string username, SignInResult result) - { - // TODO: More TODO notes in BackOfficeSignInManager - if (username.IsNullOrWhiteSpace()) - { - //TODO: this might have unwanted effects if the member is called this - username = "UNKNOWN"; - } - - if (result.Succeeded) - { - //track the last login date - user.LastLoginDateUtc = DateTime.UtcNow; - if (user.AccessFailedCount > 0) - { - //we have successfully logged in, reset the AccessFailedCount - user.AccessFailedCount = 0; - } - await UserManager.UpdateAsync(user); - - Logger.LogInformation("User: {UserName} logged in from IP address {IpAddress}", username, _ipResolver.GetCurrentRequestIpAddress()); - if (user != null) - { - //TODO: what events do we want for members? - //_memberManager.RaiseLoginSuccessEvent(Context.User, user.Id); - } - } - else if (result.IsLockedOut) - { - //TODO: what events do we want for members? - //_memberManager.RaiseAccountLockedEvent(Context.User, user.Id); - Logger.LogInformation("Login attempt failed for username {UserName} from IP address {IpAddress}, the user is locked", username, _ipResolver.GetCurrentRequestIpAddress()); - } - else if (result.RequiresTwoFactor) - { - //TODO: what events do we want for members? - //_memberManager.RaiseLoginRequiresVerificationEvent(Context.User, user.Id); - Logger.LogInformation("Login attempt requires verification for username {UserName} from IP address {IpAddress}", username, _ipResolver.GetCurrentRequestIpAddress()); - } - else if (!result.Succeeded || result.IsNotAllowed) - { - Logger.LogInformation("Login attempt failed for username {UserName} from IP address {IpAddress}", username, _ipResolver.GetCurrentRequestIpAddress()); - } - else - { - throw new ArgumentOutOfRangeException(); - } - - return result; - } } } From 5f4818263f72a77c8279d34d17ad7dd6676f2388 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 29 Mar 2021 17:37:58 +1100 Subject: [PATCH 61/78] Fixes BackOfficeClaimsPrincipalFactory to use the correct auth type. Uses the correct UmbracoIdentityRole class, fixes up MemberSignInManagerTests, new MemberClaimsPrincipalFactory --- .../BackOfficeClaimsPrincipalFactory.cs | 45 +- .../Security/ClaimsIdentityExtensions.cs | 9 + .../Security/MemberUserStore.cs | 8 +- .../Security/UmbracoIdentityRole.cs | 8 + .../Security/MemberSignInManagerTests.cs | 51 +- .../ServiceCollectionExtensions.cs | 8 +- .../Security/MemberClaimsPrincipalFactory.cs | 15 + .../Security/MemberManager.cs | 1 + .../Security/MemberSignInManager.cs | 21 - .../Security/UmbracoSignInManager.cs | 453 ++++++++++++++++++ 10 files changed, 552 insertions(+), 67 deletions(-) create mode 100644 src/Umbraco.Web.Common/Security/MemberClaimsPrincipalFactory.cs create mode 100644 src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs b/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs index 2bb9b1ab8d..9c6aeca80e 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs @@ -23,19 +23,28 @@ namespace Umbraco.Cms.Core.Security { } - /// - /// - /// Returns a ClaimsIdentity that has the required claims, and allows flowing of claims from external identity - /// - public override async Task CreateAsync(BackOfficeIdentityUser user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } + protected virtual string AuthenticationType { get; } = Constants.Security.BackOfficeAuthenticationType; + /// + protected override async Task GenerateClaimsAsync(BackOfficeIdentityUser user) + { + // NOTE: Have a look at the base implementation https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/UserClaimsPrincipalFactory.cs#L79 + // since it's setting an authentication type which is not what we want. + // so we override this method to change it. + + // get the base ClaimsIdentity baseIdentity = await base.GenerateClaimsAsync(user); + // now create a new one with the correct authentication type + var id = new ClaimsIdentity( + AuthenticationType, + Options.ClaimsIdentity.UserNameClaimType, + Options.ClaimsIdentity.RoleClaimType); + + // and merge all others from the base implementation + id.MergeAllClaims(baseIdentity); + + // ensure our required claims are there baseIdentity.AddRequiredClaims( user.Id, user.UserName, @@ -51,21 +60,7 @@ namespace Umbraco.Cms.Core.Security // assigned which could be done in the OnExternalLogin callback baseIdentity.MergeClaimsFromBackOfficeIdentity(user); - return new ClaimsPrincipal(baseIdentity); - } - - /// - protected override async Task GenerateClaimsAsync(BackOfficeIdentityUser user) - { - // TODO: Have a look at the base implementation https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/UserClaimsPrincipalFactory.cs#L79 - // since it's setting an authentication type that is probably not what we want. - // also, this is the method that we should be returning our UmbracoBackOfficeIdentity from , not the method above, - // the method above just returns a principal that wraps the identity and we dont use a custom principal, - // see https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/UserClaimsPrincipalFactory.cs#L66 - - ClaimsIdentity identity = await base.GenerateClaimsAsync(user); - - return identity; + return id; } } } diff --git a/src/Umbraco.Infrastructure/Security/ClaimsIdentityExtensions.cs b/src/Umbraco.Infrastructure/Security/ClaimsIdentityExtensions.cs index 1a37376070..d4b61a934d 100644 --- a/src/Umbraco.Infrastructure/Security/ClaimsIdentityExtensions.cs +++ b/src/Umbraco.Infrastructure/Security/ClaimsIdentityExtensions.cs @@ -13,6 +13,15 @@ namespace Umbraco.Extensions // is re-issued and we don't want to merge old values of these. private static readonly string[] s_ignoredClaims = new[] { ClaimTypes.CookiePath, Constants.Security.SessionIdClaimType }; + public static void MergeAllClaims(this ClaimsIdentity destination, ClaimsIdentity source) + { + foreach (Claim claim in source.Claims + .Where(claim => !destination.HasClaim(claim.Type, claim.Value))) + { + destination.AddClaim(new Claim(claim.Type, claim.Value)); + } + } + public static void MergeClaimsFromBackOfficeIdentity(this ClaimsIdentity destination, ClaimsIdentity source) { foreach (Claim claim in source.Claims diff --git a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs index c0b9a19ef1..7e36081e73 100644 --- a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs @@ -19,7 +19,7 @@ namespace Umbraco.Cms.Core.Security /// /// A custom user store that uses Umbraco member data /// - public class MemberUserStore : UserStoreBase, IdentityUserRole, IdentityUserLogin, IdentityUserToken, IdentityRoleClaim> + public class MemberUserStore : UserStoreBase, IdentityUserRole, IdentityUserLogin, IdentityUserToken, IdentityRoleClaim> { private const string genericIdentityErrorCode = "IdentityErrorUserStore"; private readonly IMemberService _memberService; @@ -562,7 +562,7 @@ namespace Umbraco.Cms.Core.Security } /// - protected override Task FindRoleAsync(string roleName, CancellationToken cancellationToken) + protected override Task FindRoleAsync(string roleName, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(roleName)) { @@ -572,10 +572,10 @@ namespace Umbraco.Cms.Core.Security IMemberGroup group = _memberService.GetAllRoles().SingleOrDefault(x => x.Name == roleName); if (group == null) { - return Task.FromResult((IdentityRole)null); + return Task.FromResult((UmbracoIdentityRole)null); } - return Task.FromResult(new IdentityRole(group.Name) + return Task.FromResult(new UmbracoIdentityRole(group.Name) { //TODO: what should the alias be? Id = group.Id.ToString() diff --git a/src/Umbraco.Infrastructure/Security/UmbracoIdentityRole.cs b/src/Umbraco.Infrastructure/Security/UmbracoIdentityRole.cs index 9d06dcd037..00c4038287 100644 --- a/src/Umbraco.Infrastructure/Security/UmbracoIdentityRole.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoIdentityRole.cs @@ -10,6 +10,14 @@ namespace Umbraco.Cms.Core.Models.Identity private string _id; private string _name; + public UmbracoIdentityRole(string roleName) : base(roleName) + { + } + + public UmbracoIdentityRole() + { + } + public event PropertyChangedEventHandler PropertyChanged { add diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs index 86f96c7d71..bdcc5ec75b 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs @@ -1,7 +1,11 @@ +using System; +using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; @@ -15,26 +19,45 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security [TestFixture] public class MemberSignInManagerTests { - private Mock> _mockLogger; - private readonly Mock> _memberManager = MockUserManager(); - private readonly Mock _mockIpResolver = new Mock(); + private Mock>> _mockLogger; + private readonly Mock> _memberManager = MockUserManager(); + + public MemberClaimsPrincipalFactory CreateClaimsFactory(UserManager userMgr) + => new MemberClaimsPrincipalFactory(userMgr, Options.Create(new MemberIdentityOptions())); public MemberSignInManager CreateSut() { - _mockLogger = new Mock>(); + // This all needs to be setup because internally aspnet resolves a bunch + // of services from the HttpContext.RequestServices. + var serviceProviderFactory = new DefaultServiceProviderFactory(); + var serviceCollection = new ServiceCollection(); + serviceCollection + .AddLogging() + .AddAuthentication() + .AddCookie(IdentityConstants.ApplicationScheme); + IServiceProvider serviceProvider = serviceProviderFactory.CreateServiceProvider(serviceCollection); + var httpContextFactory = new DefaultHttpContextFactory(serviceProvider); + IFeatureCollection features = new DefaultHttpContext().Features; + features.Set(new HttpConnectionFeature + { + LocalIpAddress = IPAddress.Parse("127.0.0.1") + }); + HttpContext httpContext = httpContextFactory.Create(features); + + _mockLogger = new Mock>>(); return new MemberSignInManager( _memberManager.Object, - Mock.Of(), - Mock.Of>(), + Mock.Of(x => x.HttpContext == httpContext), + CreateClaimsFactory(_memberManager.Object), Mock.Of>(), - Mock.Of>>(), + _mockLogger.Object, Mock.Of(), Mock.Of>()); } - private static Mock> MockUserManager() where TUser : class + private static Mock> MockUserManager() { - var store = new Mock>(); - var mgr = new Mock>(store.Object, null, null, null, null, null, null, null, null); + var store = new Mock>(); + var mgr = new Mock>(store.Object, null, null, null, null, null, null, null, null); return mgr; } @@ -42,8 +65,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security public async Task WhenPasswordSignInAsyncIsCalled_AndEverythingIsSetup_ThenASignInResultSucceededShouldBeReturnedAsync() { //arrange - var userId = "bo8w3d32q9b98"; - _memberManager.Setup(x => x.GetUserIdAsync(It.IsAny())).ReturnsAsync(userId); + var userId = "bo8w3d32q9b98"; MemberSignInManager sut = CreateSut(); var fakeUser = new MemberIdentityUser(777) { @@ -52,6 +74,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security var password = "testPassword"; var lockoutOnFailure = false; var isPersistent = true; + + _memberManager.Setup(x => x.GetUserIdAsync(It.IsAny())).ReturnsAsync(userId); + _memberManager.Setup(x => x.GetUserNameAsync(It.IsAny())).ReturnsAsync(fakeUser.UserName); _memberManager.Setup(x => x.FindByNameAsync(It.IsAny())).ReturnsAsync(fakeUser); _memberManager.Setup(x => x.CheckPasswordAsync(fakeUser, password)).ReturnsAsync(true); _memberManager.Setup(x => x.IsEmailConfirmedAsync(fakeUser)).ReturnsAsync(true); @@ -76,14 +101,12 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security var password = "testPassword"; var lockoutOnFailure = false; var isPersistent = true; - _mockIpResolver.Setup(x => x.GetCurrentRequestIpAddress()).Returns("127.0.0.1"); //act SignInResult actual = await sut.PasswordSignInAsync(fakeUser, password, isPersistent, lockoutOnFailure); //assert Assert.IsFalse(actual.Succeeded); - //_mockLogger.Verify(x => x.LogInformation("Login attempt failed for username TestUser from IP address 127.0.0.1", null)); } } } diff --git a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs index 5182db4e20..b970f3e551 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs @@ -12,6 +12,7 @@ using SixLabors.ImageSharp.Web.DependencyInjection; using SixLabors.ImageSharp.Web.Processors; using SixLabors.ImageSharp.Web.Providers; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Web.Common.Security; @@ -67,10 +68,11 @@ namespace Umbraco.Extensions services.BuildMembersIdentity() .AddDefaultTokenProviders() .AddMemberManager() + .AddClaimsPrincipalFactory() .AddUserStore() .AddRoleStore() - .AddRoleValidator>() - .AddRoleManager>(); + .AddRoleValidator>() + .AddRoleManager>(); private static MemberIdentityBuilder BuildMembersIdentity(this IServiceCollection services) { @@ -78,7 +80,7 @@ namespace Umbraco.Extensions services.TryAddScoped, UserValidator>(); services.TryAddScoped, PasswordValidator>(); services.TryAddScoped, PasswordHasher>(); - return new MemberIdentityBuilder(typeof(IdentityRole), services); + return new MemberIdentityBuilder(typeof(UmbracoIdentityRole), services); } private static void RemoveIntParamenterIfValueGreatherThen(IDictionary commands, string parameter, int maxValue) diff --git a/src/Umbraco.Web.Common/Security/MemberClaimsPrincipalFactory.cs b/src/Umbraco.Web.Common/Security/MemberClaimsPrincipalFactory.cs new file mode 100644 index 0000000000..fe9a0eadd4 --- /dev/null +++ b/src/Umbraco.Web.Common/Security/MemberClaimsPrincipalFactory.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Security; + + +namespace Umbraco.Cms.Web.Common.Security +{ + public class MemberClaimsPrincipalFactory : UserClaimsPrincipalFactory + { + public MemberClaimsPrincipalFactory(UserManager userManager, IOptions optionsAccessor) + : base(userManager, optionsAccessor) + { + } + } +} diff --git a/src/Umbraco.Web.Common/Security/MemberManager.cs b/src/Umbraco.Web.Common/Security/MemberManager.cs index 195312a41e..f3b80ba4bc 100644 --- a/src/Umbraco.Web.Common/Security/MemberManager.cs +++ b/src/Umbraco.Web.Common/Security/MemberManager.cs @@ -10,6 +10,7 @@ using Umbraco.Cms.Core.Security; namespace Umbraco.Cms.Web.Common.Security { + public class MemberManager : UmbracoUserManager, IMemberManager { public MemberManager( diff --git a/src/Umbraco.Web.Common/Security/MemberSignInManager.cs b/src/Umbraco.Web.Common/Security/MemberSignInManager.cs index 392d20ebbe..eeec3c2899 100644 --- a/src/Umbraco.Web.Common/Security/MemberSignInManager.cs +++ b/src/Umbraco.Web.Common/Security/MemberSignInManager.cs @@ -38,27 +38,6 @@ namespace Umbraco.Cms.Web.Common.Security // use default scheme for members protected override string TwoFactorRememberMeAuthenticationType => IdentityConstants.TwoFactorRememberMeScheme; - /// - public override async Task PasswordSignInAsync(MemberIdentityUser user, string password, bool isPersistent, bool lockoutOnFailure) - { - // overridden to handle logging/events - SignInResult result = await base.PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure); - return await HandleSignIn(user, user.UserName, result); - } - - /// - public override async Task PasswordSignInAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure) - { - // overridden to handle logging/events - MemberIdentityUser user = await UserManager.FindByNameAsync(userName); - if (user == null) - { - return await HandleSignIn(null, userName, SignInResult.Failed); - } - - return await PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure); - } - /// public override Task GetTwoFactorAuthenticationUserAsync() => throw new NotImplementedException("Two factor is not yet implemented for members"); diff --git a/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs b/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs new file mode 100644 index 0000000000..ea29098bef --- /dev/null +++ b/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs @@ -0,0 +1,453 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Models.Identity; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Web.Common.Security +{ + /// + /// Abstract sign in manager implementation allowing modifying all defeault authentication schemes + /// + /// + public abstract class UmbracoSignInManager : SignInManager + where TUser : UmbracoIdentityUser + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + protected const string UmbracoSignInMgrLoginProviderKey = "LoginProvider"; + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + protected const string UmbracoSignInMgrXsrfKey = "XsrfId"; + + public UmbracoSignInManager(UserManager userManager, IHttpContextAccessor contextAccessor, IUserClaimsPrincipalFactory claimsFactory, IOptions optionsAccessor, ILogger> logger, IAuthenticationSchemeProvider schemes, IUserConfirmation confirmation) : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) + { + } + + protected abstract string AuthenticationType { get; } + protected abstract string ExternalAuthenticationType { get; } + protected abstract string TwoFactorAuthenticationType { get; } + protected abstract string TwoFactorRememberMeAuthenticationType { get; } + + /// + public override async Task PasswordSignInAsync(TUser user, string password, bool isPersistent, bool lockoutOnFailure) + { + // override to handle logging/events + var result = await base.PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure); + return await HandleSignIn(user, user.UserName, result); + } + + /// + public override async Task GetExternalLoginInfoAsync(string expectedXsrf = null) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 + // to replace the auth scheme + + var auth = await Context.AuthenticateAsync(ExternalAuthenticationType); + var items = auth?.Properties?.Items; + if (auth?.Principal == null || items == null || !items.ContainsKey(UmbracoSignInMgrLoginProviderKey)) + { + return null; + } + + if (expectedXsrf != null) + { + if (!items.ContainsKey(UmbracoSignInMgrXsrfKey)) + { + return null; + } + var userId = items[UmbracoSignInMgrXsrfKey]; + if (userId != expectedXsrf) + { + return null; + } + } + + var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier); + if (providerKey == null || items[UmbracoSignInMgrLoginProviderKey] is not string provider) + { + return null; + } + + var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName ?? provider; + return new ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName) + { + AuthenticationTokens = auth.Properties.GetTokens(), + AuthenticationProperties = auth.Properties + }; + } + + /// + public override async Task GetTwoFactorAuthenticationUserAsync() + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + // replaced in order to use a custom auth type + + var info = await RetrieveTwoFactorInfoAsync(); + if (info == null) + { + return null; + } + return await UserManager.FindByIdAsync(info.UserId); + } + + /// + public override async Task PasswordSignInAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure) + { + // override to handle logging/events + var user = await UserManager.FindByNameAsync(userName); + if (user == null) + { + return await HandleSignIn(null, userName, SignInResult.Failed); + } + + return await PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure); + } + + /// + public override bool IsSignedIn(ClaimsPrincipal principal) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L126 + // replaced in order to use a custom auth type + + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + return principal?.Identities != null && + principal.Identities.Any(i => i.AuthenticationType == AuthenticationType); + } + + /// + public override async Task TwoFactorSignInAsync(string provider, string code, bool isPersistent, bool rememberClient) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L552 + // replaced in order to use a custom auth type and to implement logging/events + + var twoFactorInfo = await RetrieveTwoFactorInfoAsync(); + if (twoFactorInfo == null || twoFactorInfo.UserId == null) + { + return SignInResult.Failed; + } + var user = await UserManager.FindByIdAsync(twoFactorInfo.UserId); + if (user == null) + { + return SignInResult.Failed; + } + + var error = await PreSignInCheck(user); + if (error != null) + { + return error; + } + if (await UserManager.VerifyTwoFactorTokenAsync(user, provider, code)) + { + await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent, rememberClient); + return await HandleSignIn(user, user?.UserName, SignInResult.Success); + } + // If the token is incorrect, record the failure which also may cause the user to be locked out + await UserManager.AccessFailedAsync(user); + return await HandleSignIn(user, user?.UserName, SignInResult.Failed); + } + + /// + public override async Task RefreshSignInAsync(TUser user) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L126 + // replaced in order to use a custom auth type + + var auth = await Context.AuthenticateAsync(AuthenticationType); + IList claims = Array.Empty(); + + var authenticationMethod = auth?.Principal?.FindFirst(ClaimTypes.AuthenticationMethod); + var amr = auth?.Principal?.FindFirst("amr"); + + if (authenticationMethod != null || amr != null) + { + claims = new List(); + if (authenticationMethod != null) + { + claims.Add(authenticationMethod); + } + if (amr != null) + { + claims.Add(amr); + } + } + + await SignInWithClaimsAsync(user, auth?.Properties, claims); + } + + /// + public override async Task SignInWithClaimsAsync(TUser user, AuthenticationProperties authenticationProperties, IEnumerable additionalClaims) + { + // override to replace IdentityConstants.ApplicationScheme with custom AuthenticationType + // code taken from aspnetcore: https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + // we also override to set the current HttpContext principal since this isn't done by default + + var userPrincipal = await CreateUserPrincipalAsync(user); + foreach (var claim in additionalClaims) + { + userPrincipal.Identities.First().AddClaim(claim); + } + + // FYI (just for informational purposes): + // This calls an ext method will eventually reaches `IAuthenticationService.SignInAsync` + // which then resolves the `IAuthenticationSignInHandler` for the current scheme + // by calling `IAuthenticationHandlerProvider.GetHandlerAsync(context, scheme);` + // which then calls `IAuthenticationSignInHandler.SignInAsync` = CookieAuthenticationHandler.HandleSignInAsync + + // Also note, that when the CookieAuthenticationHandler sign in is successful we handle that event within our + // own ConfigureUmbracoBackOfficeCookieOptions which assigns the current HttpContext.User to the IPrincipal created + + // Also note, this method gets called when performing 2FA logins + + await Context.SignInAsync( + AuthenticationType, + userPrincipal, + authenticationProperties ?? new AuthenticationProperties()); + } + + /// + public override async Task SignOutAsync() + { + // override to replace IdentityConstants.ApplicationScheme with custom auth types + // code taken from aspnetcore: https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + + await Context.SignOutAsync(AuthenticationType); + await Context.SignOutAsync(ExternalAuthenticationType); + await Context.SignOutAsync(TwoFactorAuthenticationType); + } + + /// + public override async Task IsTwoFactorClientRememberedAsync(TUser user) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 + // to replace the auth scheme + + var userId = await UserManager.GetUserIdAsync(user); + var result = await Context.AuthenticateAsync(TwoFactorRememberMeAuthenticationType); + return (result?.Principal != null && result.Principal.FindFirstValue(ClaimTypes.Name) == userId); + } + + /// + public override async Task RememberTwoFactorClientAsync(TUser user) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 + // to replace the auth scheme + + var principal = await StoreRememberClient(user); + await Context.SignInAsync(TwoFactorRememberMeAuthenticationType, + principal, + new AuthenticationProperties { IsPersistent = true }); + } + + /// + public override async Task TwoFactorRecoveryCodeSignInAsync(string recoveryCode) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 + // to replace the auth scheme + + var twoFactorInfo = await RetrieveTwoFactorInfoAsync(); + if (twoFactorInfo == null || twoFactorInfo.UserId == null) + { + return SignInResult.Failed; + } + var user = await UserManager.FindByIdAsync(twoFactorInfo.UserId); + if (user == null) + { + return SignInResult.Failed; + } + + var result = await UserManager.RedeemTwoFactorRecoveryCodeAsync(user, recoveryCode); + if (result.Succeeded) + { + await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent: false, rememberClient: false); + return SignInResult.Success; + } + + // We don't protect against brute force attacks since codes are expected to be random. + return SignInResult.Failed; + } + + /// + public override Task ForgetTwoFactorClientAsync() + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 + // to replace the auth scheme + + return Context.SignOutAsync(TwoFactorRememberMeAuthenticationType); + } + + /// + /// Called on any login attempt to update the AccessFailedCount and to raise events + /// + /// + /// + /// + /// + protected virtual async Task HandleSignIn(TUser user, string username, SignInResult result) + { + // TODO: Here I believe we can do all (or most) of the usermanager event raising so that it is not in the AuthenticationController + + if (username.IsNullOrWhiteSpace()) + { + username = "UNKNOWN"; // could happen in 2fa or something else weird + } + + if (result.Succeeded) + { + //track the last login date + user.LastLoginDateUtc = DateTime.UtcNow; + if (user.AccessFailedCount > 0) + { + //we have successfully logged in, reset the AccessFailedCount + user.AccessFailedCount = 0; + } + await UserManager.UpdateAsync(user); + + Logger.LogInformation("User: {UserName} logged in from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress); + } + else if (result.IsLockedOut) + { + Logger.LogInformation("Login attempt failed for username {UserName} from IP address {IpAddress}, the user is locked", username, Context.Connection.RemoteIpAddress); + } + else if (result.RequiresTwoFactor) + { + Logger.LogInformation("Login attempt requires verification for username {UserName} from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress); + } + else if (!result.Succeeded || result.IsNotAllowed) + { + Logger.LogInformation("Login attempt failed for username {UserName} from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress); + } + else + { + throw new ArgumentOutOfRangeException(); + } + + return result; + } + + /// + protected override async Task SignInOrTwoFactorAsync(TUser user, bool isPersistent, string loginProvider = null, bool bypassTwoFactor = false) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + // to replace custom auth types + + if (!bypassTwoFactor && await IsTfaEnabled(user)) + { + if (!await IsTwoFactorClientRememberedAsync(user)) + { + // Store the userId for use after two factor check + var userId = await UserManager.GetUserIdAsync(user); + await Context.SignInAsync(IdentityConstants.TwoFactorUserIdScheme, StoreTwoFactorInfo(userId, loginProvider)); + return SignInResult.TwoFactorRequired; + } + } + // Cleanup external cookie + if (loginProvider != null) + { + await Context.SignOutAsync(ExternalAuthenticationType); + } + if (loginProvider == null) + { + await SignInWithClaimsAsync(user, isPersistent, new Claim[] { new Claim("amr", "pwd") }); + } + else + { + await SignInAsync(user, isPersistent, loginProvider); + } + return SignInResult.Success; + } + + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L782 + // since it's not public + private async Task IsTfaEnabled(TUser user) + => UserManager.SupportsUserTwoFactor && + await UserManager.GetTwoFactorEnabledAsync(user) && + (await UserManager.GetValidTwoFactorProvidersAsync(user)).Count > 0; + + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L743 + // to replace custom auth types + private ClaimsPrincipal StoreTwoFactorInfo(string userId, string loginProvider) + { + var identity = new ClaimsIdentity(TwoFactorAuthenticationType); + identity.AddClaim(new Claim(ClaimTypes.Name, userId)); + if (loginProvider != null) + { + identity.AddClaim(new Claim(ClaimTypes.AuthenticationMethod, loginProvider)); + } + return new ClaimsPrincipal(identity); + } + + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + // copy is required in order to use custom auth types + private async Task StoreRememberClient(TUser user) + { + var userId = await UserManager.GetUserIdAsync(user); + var rememberBrowserIdentity = new ClaimsIdentity(TwoFactorRememberMeAuthenticationType); + rememberBrowserIdentity.AddClaim(new Claim(ClaimTypes.Name, userId)); + if (UserManager.SupportsUserSecurityStamp) + { + var stamp = await UserManager.GetSecurityStampAsync(user); + rememberBrowserIdentity.AddClaim(new Claim(Options.ClaimsIdentity.SecurityStampClaimType, stamp)); + } + return new ClaimsPrincipal(rememberBrowserIdentity); + } + + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + // copy is required in order to use a custom auth type + private async Task RetrieveTwoFactorInfoAsync() + { + var result = await Context.AuthenticateAsync(TwoFactorAuthenticationType); + if (result?.Principal != null) + { + return new TwoFactorAuthenticationInfo + { + UserId = result.Principal.FindFirstValue(ClaimTypes.Name), + LoginProvider = result.Principal.FindFirstValue(ClaimTypes.AuthenticationMethod) + }; + } + return null; + } + + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + // copy is required in order to use custom auth types + private async Task DoTwoFactorSignInAsync(TUser user, TwoFactorAuthenticationInfo twoFactorInfo, bool isPersistent, bool rememberClient) + { + // When token is verified correctly, clear the access failed count used for lockout + await ResetLockout(user); + + var claims = new List + { + new Claim("amr", "mfa") + }; + + // Cleanup external cookie + if (twoFactorInfo.LoginProvider != null) + { + claims.Add(new Claim(ClaimTypes.AuthenticationMethod, twoFactorInfo.LoginProvider)); + await Context.SignOutAsync(ExternalAuthenticationType); + } + // Cleanup two factor user id cookie + await Context.SignOutAsync(TwoFactorAuthenticationType); + if (rememberClient) + { + await RememberTwoFactorClientAsync(user); + } + await SignInWithClaimsAsync(user, isPersistent, claims); + } + + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L891 + private class TwoFactorAuthenticationInfo + { + public string UserId { get; set; } + public string LoginProvider { get; set; } + } + } +} From 9fac0a147018a0e546616eb3caa15082a30c0c13 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 29 Mar 2021 18:05:13 +1100 Subject: [PATCH 62/78] Fixes tests --- .../Security/BackOfficeClaimsPrincipalFactory.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs b/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs index 9c6aeca80e..c86bda44e0 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs @@ -45,7 +45,7 @@ namespace Umbraco.Cms.Core.Security id.MergeAllClaims(baseIdentity); // ensure our required claims are there - baseIdentity.AddRequiredClaims( + id.AddRequiredClaims( user.Id, user.UserName, user.Name, @@ -58,7 +58,7 @@ namespace Umbraco.Cms.Core.Security // now we can flow any custom claims that the actual user has currently // assigned which could be done in the OnExternalLogin callback - baseIdentity.MergeClaimsFromBackOfficeIdentity(user); + id.MergeClaimsFromBackOfficeIdentity(user); return id; } From 4ef6c798fc57297ca54cc1eaf3e77d6146c4119f Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Mon, 29 Mar 2021 08:48:42 +0100 Subject: [PATCH 63/78] Remove leftover comments from migrating this over --- .../Services/Implement/DataTypeService.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs b/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs index b96f54dc14..fd1b40eb2f 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs @@ -346,7 +346,6 @@ namespace Umbraco.Cms.Core.Services.Implement using (var scope = ScopeProvider.CreateScope()) { var moveEventInfo = new MoveEventInfo(toMove, toMove.Path, parentId); - // var moveEventArgs = new MoveEventArgs(evtMsgs, moveEventInfo); var movingDataTypeNotification = new DataTypeMovingNotification(moveEventInfo, evtMsgs); if (scope.Notifications.PublishCancelable(movingDataTypeNotification)) @@ -440,7 +439,6 @@ namespace Umbraco.Cms.Core.Services.Implement { var evtMsgs = EventMessagesFactory.Get(); var dataTypeDefinitionsA = dataTypeDefinitions.ToArray(); - //var saveEventArgs = new SaveEventArgs(dataTypeDefinitionsA); using (var scope = ScopeProvider.CreateScope()) { From 6d73051beaa4321ff135b348df94d9870868ece1 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Mon, 29 Mar 2021 09:19:45 +0100 Subject: [PATCH 64/78] Fixup failing unit test - we no longer using the EventArgs --- .../Cache/DistributedCacheBinderTests.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs b/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs index 6a263cb6ae..0b38e849c8 100644 --- a/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs +++ b/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs @@ -47,9 +47,6 @@ namespace Umbraco.Cms.Tests.Integration.Cache new EventDefinition>(null, UserService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, UserService, new DeleteEventArgs(Enumerable.Empty())), - new EventDefinition>(null, DataTypeService, new SaveEventArgs(Enumerable.Empty())), - new EventDefinition>(null, DataTypeService, new DeleteEventArgs(Enumerable.Empty())), - new EventDefinition>(null, FileService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, FileService, new DeleteEventArgs(Enumerable.Empty())), From b4cbf0cf5dbeede1c55f793d243996c94b5f5cfc Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 29 Mar 2021 10:42:42 +0200 Subject: [PATCH 65/78] Remove obsolete static events --- .../Cache/DistributedCacheBinder_Handlers.cs | 68 +++++++++---------- .../Compose/NotificationsComposer.cs | 10 ++- .../Services/Implement/MemberService.cs | 15 +--- .../Services/Implement/PublicAccessService.cs | 14 +--- .../Services/Implement/UserService.cs | 22 +----- .../Cache/DistributedCacheBinderTests.cs | 11 --- .../Scoping/ScopedRepositoryTests.cs | 19 +++--- 7 files changed, 55 insertions(+), 104 deletions(-) diff --git a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs index e8479de307..a1071b71f0 100644 --- a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs +++ b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs @@ -23,7 +23,15 @@ namespace Umbraco.Cms.Core.Cache INotificationHandler, INotificationHandler, INotificationHandler, - INotificationHandler + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler { private List _unbinders; @@ -54,16 +62,6 @@ namespace Umbraco.Cms.Core.Cache _logger.LogInformation("Initializing Umbraco internal event handlers for cache refreshing."); - // bind to user and user group events - Bind(() => UserService.SavedUserGroup += UserService_SavedUserGroup, - () => UserService.SavedUserGroup -= UserService_SavedUserGroup); - Bind(() => UserService.DeletedUserGroup += UserService_DeletedUserGroup, - () => UserService.DeletedUserGroup -= UserService_DeletedUserGroup); - Bind(() => UserService.SavedUser += UserService_SavedUser, - () => UserService.SavedUser -= UserService_SavedUser); - Bind(() => UserService.DeletedUser += UserService_DeletedUser, - () => UserService.DeletedUser -= UserService_DeletedUser); - // bind to data type events Bind(() => DataTypeService.Deleted += DataTypeService_Deleted, () => DataTypeService.Deleted -= DataTypeService_Deleted); @@ -103,10 +101,6 @@ namespace Umbraco.Cms.Core.Cache () => MacroService.Deleted -= MacroService_Deleted); // bind to member events - Bind(() => MemberService.Saved += MemberService_Saved, - () => MemberService.Saved -= MemberService_Saved); - Bind(() => MemberService.Deleted += MemberService_Deleted, - () => MemberService.Deleted -= MemberService_Deleted); Bind(() => MemberGroupService.Saved += MemberGroupService_Saved, () => MemberGroupService.Saved -= MemberGroupService_Saved); Bind(() => MemberGroupService.Deleted += MemberGroupService_Deleted, @@ -126,12 +120,6 @@ namespace Umbraco.Cms.Core.Cache //Bind(() => ContentService.DeletedBlueprint += ContentService_DeletedBlueprint, // () => ContentService.DeletedBlueprint -= ContentService_DeletedBlueprint); - // bind to public access events - Bind(() => PublicAccessService.Saved += PublicAccessService_Saved, - () => PublicAccessService.Saved -= PublicAccessService_Saved); - Bind(() => PublicAccessService.Deleted += PublicAccessService_Deleted, - () => PublicAccessService.Deleted -= PublicAccessService_Deleted); - // bind to relation type events Bind(() => RelationService.SavedRelationType += RelationService_SavedRelationType, () => RelationService.SavedRelationType -= RelationService_SavedRelationType); @@ -141,14 +129,15 @@ namespace Umbraco.Cms.Core.Cache #region PublicAccessService - private void PublicAccessService_Saved(IPublicAccessService sender, SaveEventArgs e) + public void Handle(PublicAccessEntrySavedNotification notification) { _distributedCache.RefreshPublicAccess(); } - private void PublicAccessService_Deleted(IPublicAccessService sender, DeleteEventArgs e) + public void Handle(PublicAccessEntryDeletedNotification notification) { _distributedCache.RefreshPublicAccess(); + } #endregion @@ -293,29 +282,36 @@ namespace Umbraco.Cms.Core.Cache #region UserService - private void UserService_SavedUser(IUserService sender, SaveEventArgs e) + public void Handle(UserSavedNotification notification) { - foreach (var entity in e.SavedEntities) + foreach (IUser entity in notification.SavedEntities) + { _distributedCache.RefreshUserCache(entity.Id); + } } - private void UserService_DeletedUser(IUserService sender, DeleteEventArgs e) + public void Handle(UserDeletedNotification notification) { - foreach (var entity in e.DeletedEntities) + foreach (IUser entity in notification.DeletedEntities) + { _distributedCache.RemoveUserCache(entity.Id); + } } - private void UserService_SavedUserGroup(IUserService sender, SaveEventArgs e) + public void Handle(UserGroupWithUsersSavedNotification notification) { - foreach (var entity in e.SavedEntities) + foreach (UserGroupWithUsers entity in notification.SavedEntities) + { _distributedCache.RefreshUserGroupCache(entity.UserGroup.Id); + } } - private void UserService_DeletedUserGroup(IUserService sender, DeleteEventArgs e) + public void Handle(UserGroupDeletedNotification notification) { - - foreach (var entity in e.DeletedEntities) + foreach (IUserGroup entity in notification.DeletedEntities) + { _distributedCache.RemoveUserGroupCache(entity.Id); + } } #endregion @@ -377,14 +373,14 @@ namespace Umbraco.Cms.Core.Cache #region MemberService - private void MemberService_Deleted(IMemberService sender, DeleteEventArgs e) + public void Handle(MemberDeletedNotification notification) { - _distributedCache.RemoveMemberCache(e.DeletedEntities.ToArray()); + _distributedCache.RemoveMemberCache(notification.DeletedEntities.ToArray()); } - private void MemberService_Saved(IMemberService sender, SaveEventArgs e) + public void Handle(MemberSavedNotification notification) { - _distributedCache.RefreshMemberCache(e.SavedEntities.ToArray()); + _distributedCache.RefreshMemberCache(notification.SavedEntities.ToArray()); } #endregion diff --git a/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs b/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs index fd35a5111f..be6539ac55 100644 --- a/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs +++ b/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs @@ -70,7 +70,15 @@ namespace Umbraco.Cms.Core.Compose .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() - .AddNotificationHandler(); + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler(); // add notification handlers for auditing builder diff --git a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs index 794fd09934..aba40333b0 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; @@ -796,7 +796,6 @@ namespace Umbraco.Cms.Core.Services.Implement if (raiseEvents) { scope.Notifications.Publish(new MemberSavedNotification(member, evtMsgs).WithStateFrom(savingNotification)); - scope.Events.Dispatch(Saved, this, new SaveEventArgs(member, false)); } Audit(AuditType.Save, 0, member.Id); @@ -835,7 +834,6 @@ namespace Umbraco.Cms.Core.Services.Implement if (raiseEvents) { scope.Notifications.Publish(new MemberSavedNotification(membersA, evtMsgs).WithStateFrom(savingNotification)); - scope.Events.Dispatch(Saved, this, new SaveEventArgs(membersA, false)); } Audit(AuditType.Save, 0, -1, "Save multiple Members"); @@ -877,7 +875,6 @@ namespace Umbraco.Cms.Core.Services.Implement // a member has no descendants _memberRepository.Delete(member); scope.Notifications.Publish(new MemberDeletedNotification(member, evtMsgs).WithState(notificationState)); - scope.Events.Dispatch(Deleted, this, new DeleteEventArgs(member, false)); // media files deleted by QueuingEventDispatcher } @@ -1073,16 +1070,6 @@ namespace Umbraco.Cms.Core.Services.Implement #endregion - #region Event Handlers - - [Obsolete("Will be removed in an upcoming version. Implement an INotificationHandler for MemberDeletedNotification instead.")] - public static event TypedEventHandler> Deleted; - - [Obsolete("Will be removed in an upcoming version. Implement an INotificationHandler for MemberSavedNotification instead.")] - public static event TypedEventHandler> Saved; - - #endregion - #region Membership diff --git a/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs b/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs index 3d44ed2cf8..b6d12784c6 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs @@ -11,7 +11,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Core.Services.Implement { - public class PublicAccessService : RepositoryService, IPublicAccessService + internal class PublicAccessService : RepositoryService, IPublicAccessService { private readonly IPublicAccessRepository _publicAccessRepository; @@ -115,7 +115,7 @@ namespace Umbraco.Cms.Core.Services.Implement { entry = _publicAccessRepository.GetMany().FirstOrDefault(x => x.ProtectedNodeId == content.Id); if (entry == null) - return OperationResult.Attempt.Cannot(evtMsgs); // causes rollback // causes rollback + return OperationResult.Attempt.Cannot(evtMsgs); // causes rollback var existingRule = entry.Rules.FirstOrDefault(x => x.RuleType == ruleType && x.RuleValue == ruleValue); if (existingRule == null) @@ -140,7 +140,6 @@ namespace Umbraco.Cms.Core.Services.Implement scope.Complete(); scope.Notifications.Publish(new PublicAccessEntrySavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); - scope.Events.Dispatch(Saved, this, new SaveEventArgs(entry, false)); } return OperationResult.Attempt.Succeed(evtMsgs, entry); @@ -177,7 +176,6 @@ namespace Umbraco.Cms.Core.Services.Implement scope.Complete(); scope.Notifications.Publish(new PublicAccessEntrySavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); - scope.Events.Dispatch(Saved, this, new SaveEventArgs(entry, false)); } return OperationResult.Attempt.Succeed(evtMsgs); @@ -204,7 +202,6 @@ namespace Umbraco.Cms.Core.Services.Implement scope.Complete(); scope.Notifications.Publish(new PublicAccessEntrySavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); - scope.Events.Dispatch(Saved, this, new SaveEventArgs(entry, false)); } return OperationResult.Attempt.Succeed(evtMsgs); @@ -231,16 +228,9 @@ namespace Umbraco.Cms.Core.Services.Implement scope.Complete(); scope.Notifications.Publish(new PublicAccessEntryDeletedNotification(entry, evtMsgs).WithStateFrom(deletingNotification)); - scope.Events.Dispatch(Deleted, this, new DeleteEventArgs(entry, false)); } return OperationResult.Attempt.Succeed(evtMsgs); } - - [Obsolete("Will be removed in an upcoming version. Implement an INotificationHandler for PublicAccessEntrySavedNotification instead.")] - public static event TypedEventHandler> Saved; - - [Obsolete("Will be removed in an upcoming version. Implement an INotificationHandler for PublicAccessEntryDeletedNotification instead.")] - public static event TypedEventHandler> Deleted; } } diff --git a/src/Umbraco.Infrastructure/Services/Implement/UserService.cs b/src/Umbraco.Infrastructure/Services/Implement/UserService.cs index e29eeefc01..b7b52ae616 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/UserService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/UserService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Data.Common; using System.Globalization; @@ -22,7 +22,7 @@ namespace Umbraco.Cms.Core.Services.Implement /// /// Represents the UserService, which is an easy access to operations involving , and eventually Backoffice Users. /// - public class UserService : RepositoryService, IUserService + internal class UserService : RepositoryService, IUserService { private readonly IUserRepository _userRepository; private readonly IUserGroupRepository _userGroupRepository; @@ -142,7 +142,6 @@ namespace Umbraco.Cms.Core.Services.Implement _userRepository.Save(user); scope.Notifications.Publish(new UserSavedNotification(user, evtMsgs).WithStateFrom(savingNotification)); - scope.Events.Dispatch(SavedUser, this, new SaveEventArgs(user, false)); scope.Complete(); } @@ -256,7 +255,6 @@ namespace Umbraco.Cms.Core.Services.Implement _userRepository.Delete(user); scope.Notifications.Publish(new UserDeletedNotification(user, evtMsgs).WithStateFrom(deletingNotification)); - scope.Events.Dispatch(DeletedUser, this, new DeleteEventArgs(user, false)); scope.Complete(); } } @@ -300,7 +298,6 @@ namespace Umbraco.Cms.Core.Services.Implement if (raiseEvents) { scope.Notifications.Publish(new UserSavedNotification(entity, evtMsgs).WithStateFrom(savingNotification)); - scope.Events.Dispatch(SavedUser, this, new SaveEventArgs(entity, false)); } scope.Complete(); @@ -354,7 +351,6 @@ namespace Umbraco.Cms.Core.Services.Implement if (raiseEvents) { scope.Notifications.Publish(new UserSavedNotification(entitiesA, evtMsgs).WithStateFrom(savingNotification)); - scope.Events.Dispatch(SavedUser, this, new SaveEventArgs(entitiesA, false)); } //commit the whole lot in one go @@ -865,7 +861,6 @@ namespace Umbraco.Cms.Core.Services.Implement { scope.Notifications.Publish(new UserGroupSavedNotification(userGroup, evtMsgs).WithStateFrom(savingNotification)); scope.Notifications.Publish(new UserGroupWithUsersSavedNotification(userGroupWithUsers, evtMsgs).WithStateFrom(savingUserGroupWithUsersNotification)); - scope.Events.Dispatch(SavedUserGroup, this, new SaveEventArgs(new UserGroupWithUsers(userGroup, addedUsers, removedUsers), false)); } scope.Complete(); @@ -892,7 +887,6 @@ namespace Umbraco.Cms.Core.Services.Implement _userGroupRepository.Delete(userGroup); scope.Notifications.Publish(new UserGroupDeletedNotification(userGroup, evtMsgs).WithStateFrom(deletingNotification)); - scope.Events.Dispatch(DeletedUserGroup, this, new DeleteEventArgs(userGroup, false)); scope.Complete(); } @@ -1172,17 +1166,5 @@ namespace Umbraco.Cms.Core.Services.Implement } #endregion - - [Obsolete("Will be removed in an upcoming version. Implement an INotificationHandler for UserSavedNotification instead.")] - public static event TypedEventHandler> SavedUser; - - [Obsolete("Will be removed in an upcoming version. Implement an INotificationHandler for UserDeletedNotification instead.")] - public static event TypedEventHandler> DeletedUser; - - [Obsolete("Will be removed in an upcoming version. Implement an INotificationHandler for UserGroupSavedNotification instead.")] - public static event TypedEventHandler> SavedUserGroup; - - [Obsolete("Will be removed in an upcoming version. Implement an INotificationHandler for UserGroupDeletedNotification instead.")] - public static event TypedEventHandler> DeletedUserGroup; } } diff --git a/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs b/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs index 6a263cb6ae..b0d62778cf 100644 --- a/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs +++ b/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs @@ -42,11 +42,6 @@ namespace Umbraco.Cms.Tests.Integration.Cache var definitions = new IEventDefinition[] { - new EventDefinition>(null, UserService, new SaveEventArgs(Enumerable.Empty())), - new EventDefinition>(null, UserService, new DeleteEventArgs(Enumerable.Empty())), - new EventDefinition>(null, UserService, new SaveEventArgs(Enumerable.Empty())), - new EventDefinition>(null, UserService, new DeleteEventArgs(Enumerable.Empty())), - new EventDefinition>(null, DataTypeService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, DataTypeService, new DeleteEventArgs(Enumerable.Empty())), @@ -70,9 +65,6 @@ namespace Umbraco.Cms.Tests.Integration.Cache new EventDefinition>(null, MacroService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, MacroService, new DeleteEventArgs(Enumerable.Empty())), - new EventDefinition>(null, MemberService, new SaveEventArgs(Enumerable.Empty())), - new EventDefinition>(null, MemberService, new DeleteEventArgs(Enumerable.Empty())), - new EventDefinition>(null, MemberGroupService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, MemberGroupService, new DeleteEventArgs(Enumerable.Empty())), @@ -80,9 +72,6 @@ namespace Umbraco.Cms.Tests.Integration.Cache //new EventDefinition>(null, ContentService, new SaveEventArgs(Enumerable.Empty()), "SavedBlueprint"), //new EventDefinition>(null, ContentService, new DeleteEventArgs(Enumerable.Empty()), "DeletedBlueprint"), - new EventDefinition>(null, PublicAccessService, new SaveEventArgs(Enumerable.Empty())), - new EventDefinition>(null, PublicAccessService, new DeleteEventArgs(Enumerable.Empty())), - new EventDefinition>(null, RelationService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, RelationService, new DeleteEventArgs(Enumerable.Empty())), diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs index 61334940e1..695a281d0a 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using Microsoft.Extensions.Logging; using NUnit.Framework; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.DependencyInjection; @@ -14,7 +13,6 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Implement; using Umbraco.Cms.Core.Sync; -using Umbraco.Cms.Core.Web; using Umbraco.Cms.Infrastructure.PublishedCache; using Umbraco.Cms.Infrastructure.Services.Notifications; using Umbraco.Cms.Infrastructure.Sync; @@ -55,7 +53,8 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() - .AddNotificationHandler(); + .AddNotificationHandler() + .AddNotificationHandler(); builder.AddNotificationHandler(); } @@ -76,18 +75,18 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping var user = (IUser)new User(GlobalSettings, "name", "email", "username", "rawPassword"); service.Save(user); - // global cache contains the entity + // User has been saved so the cache has been cleared of it var globalCached = (IUser)globalCache.Get(GetCacheIdKey(user.Id), () => null); + Assert.IsNull(globalCached); + // Get user again to load it into the cache again, this also ensure we don't modify the one that's in the cache. + user = service.GetUserById(user.Id); + + // global cache contains the entity + globalCached = (IUser)globalCache.Get(GetCacheIdKey(user.Id), () => null); Assert.IsNotNull(globalCached); Assert.AreEqual(user.Id, globalCached.Id); Assert.AreEqual("name", globalCached.Name); - // get user again - else we'd modify the one that's in the cache - user = service.GetUserById(user.Id); - - _distributedCacheBinder = new DistributedCacheBinder(new DistributedCache(ServerMessenger, CacheRefresherCollection), GetRequiredService(), GetRequiredService>()); - _distributedCacheBinder.BindEvents(true); - Assert.IsNull(scopeProvider.AmbientScope); using (IScope scope = scopeProvider.CreateScope(repositoryCacheMode: RepositoryCacheMode.Scoped)) { From 44b90d5993d377c5857e26183261a3947a68d23e Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Mon, 29 Mar 2021 11:08:14 +0100 Subject: [PATCH 66/78] Implment a Renaming & Renamed notification for DataType Container --- .../EntityContainerRenamedNotification.cs | 2 +- .../EntityContainerRenamingNotification.cs | 11 ++++++++++ .../Events/RenamedNotification.cs | 20 +++++++++++++++++++ .../Events/RenamingNotification.cs | 20 +++++++++++++++++++ .../Services/Implement/DataTypeService.cs | 9 ++++++++- 5 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 src/Umbraco.Core/Events/EntityContainerRenamingNotification.cs create mode 100644 src/Umbraco.Core/Events/RenamedNotification.cs create mode 100644 src/Umbraco.Core/Events/RenamingNotification.cs diff --git a/src/Umbraco.Core/Events/EntityContainerRenamedNotification.cs b/src/Umbraco.Core/Events/EntityContainerRenamedNotification.cs index 3fac1fdd40..dc1b858bd9 100644 --- a/src/Umbraco.Core/Events/EntityContainerRenamedNotification.cs +++ b/src/Umbraco.Core/Events/EntityContainerRenamedNotification.cs @@ -2,7 +2,7 @@ using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.Events { - public class EntityContainerRenamedNotification : SavedNotification + public class EntityContainerRenamedNotification : RenamedNotification { public EntityContainerRenamedNotification(EntityContainer target, EventMessages messages) : base(target, messages) { diff --git a/src/Umbraco.Core/Events/EntityContainerRenamingNotification.cs b/src/Umbraco.Core/Events/EntityContainerRenamingNotification.cs new file mode 100644 index 0000000000..f2408a1faf --- /dev/null +++ b/src/Umbraco.Core/Events/EntityContainerRenamingNotification.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Events +{ + public class EntityContainerRenamingNotification : RenamingNotification + { + public EntityContainerRenamingNotification(EntityContainer target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Core/Events/RenamedNotification.cs b/src/Umbraco.Core/Events/RenamedNotification.cs new file mode 100644 index 0000000000..81a568b6df --- /dev/null +++ b/src/Umbraco.Core/Events/RenamedNotification.cs @@ -0,0 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; + +namespace Umbraco.Cms.Core.Events +{ + public abstract class RenamedNotification : EnumerableObjectNotification + { + protected RenamedNotification(T target, EventMessages messages) : base(target, messages) + { + } + + protected RenamedNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + + public IEnumerable Entities => Target; + } +} diff --git a/src/Umbraco.Core/Events/RenamingNotification.cs b/src/Umbraco.Core/Events/RenamingNotification.cs new file mode 100644 index 0000000000..f215a8ea54 --- /dev/null +++ b/src/Umbraco.Core/Events/RenamingNotification.cs @@ -0,0 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; + +namespace Umbraco.Cms.Core.Events +{ + public abstract class RenamingNotification : CancelableEnumerableObjectNotification + { + protected RenamingNotification(T target, EventMessages messages) : base(target, messages) + { + } + + protected RenamingNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + + public IEnumerable Entities => Target; + } +} diff --git a/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs b/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs index fd1b40eb2f..455f88a70f 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs @@ -223,10 +223,17 @@ namespace Umbraco.Cms.Core.Services.Implement container.Name = name; + var renamingEntityContainerNotification = new EntityContainerRenamingNotification(container, evtMsgs); + if (scope.Notifications.PublishCancelable(renamingEntityContainerNotification)) + { + scope.Complete(); + return OperationResult.Attempt.Cancel(evtMsgs, container); + } + _dataTypeContainerRepository.Save(container); scope.Complete(); - scope.Notifications.Publish(new EntityContainerRenamedNotification(container, evtMsgs)); + scope.Notifications.Publish(new EntityContainerRenamedNotification(container, evtMsgs).WithStateFrom(renamingEntityContainerNotification)); return OperationResult.Attempt.Succeed(OperationResultType.Success, evtMsgs, container); } From 62f62518f0a2d0dd301db1275458d5ac958969b2 Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 29 Mar 2021 13:38:46 +0200 Subject: [PATCH 67/78] Make MemberGroupService internal --- .../Services/Implement/MemberGroupService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Infrastructure/Services/Implement/MemberGroupService.cs b/src/Umbraco.Infrastructure/Services/Implement/MemberGroupService.cs index ac3366a065..ceed54d962 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; @@ -10,7 +10,7 @@ using Umbraco.Cms.Infrastructure.Services.Notifications; namespace Umbraco.Cms.Core.Services.Implement { - public class MemberGroupService : RepositoryService, IMemberGroupService + internal class MemberGroupService : RepositoryService, IMemberGroupService { private readonly IMemberGroupRepository _memberGroupRepository; From 4c50c57c1f34720fe76faaff6452ea429e304e6c Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Tue, 30 Mar 2021 08:48:45 +0100 Subject: [PATCH 68/78] Remove unused eventAggregator --- .../Services/Implement/DataTypeService.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs b/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs index 455f88a70f..4bb1e46c29 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs @@ -31,14 +31,13 @@ namespace Umbraco.Cms.Core.Services.Implement private readonly ILocalizationService _localizationService; private readonly IShortStringHelper _shortStringHelper; private readonly IJsonSerializer _jsonSerializer; - private readonly IEventAggregator _eventAggregator; public DataTypeService(IScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IDataTypeRepository dataTypeRepository, IDataTypeContainerRepository dataTypeContainerRepository, IAuditRepository auditRepository, IEntityRepository entityRepository, IContentTypeRepository contentTypeRepository, IIOHelper ioHelper, ILocalizedTextService localizedTextService, ILocalizationService localizationService, IShortStringHelper shortStringHelper, - IJsonSerializer jsonSerializer, IEventAggregator eventAggregator) + IJsonSerializer jsonSerializer) : base(provider, loggerFactory, eventMessagesFactory) { _dataTypeRepository = dataTypeRepository; @@ -51,7 +50,6 @@ namespace Umbraco.Cms.Core.Services.Implement _localizationService = localizationService; _shortStringHelper = shortStringHelper; _jsonSerializer = jsonSerializer; - _eventAggregator = eventAggregator; } #region Containers From 7b34b495afbd986ab07a9688b1587e2c4938ba4b Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Tue, 30 Mar 2021 09:25:34 +0100 Subject: [PATCH 69/78] Implement the events now as notifications for DataType Service --- .../Cache/DistributedCacheBinder_Handlers.cs | 18 +++++++----------- .../Cache/DistributedCacheBinderTests.cs | 3 --- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs index 0782579497..a6b0dd73db 100644 --- a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs +++ b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs @@ -33,7 +33,9 @@ namespace Umbraco.Cms.Core.Cache INotificationHandler, INotificationHandler, INotificationHandler, - INotificationHandler + INotificationHandler, + INotificationHandler, + INotificationHandler { private List _unbinders; @@ -64,12 +66,6 @@ namespace Umbraco.Cms.Core.Cache _logger.LogInformation("Initializing Umbraco internal event handlers for cache refreshing."); - // bind to data type events - Bind(() => DataTypeService.Deleted += DataTypeService_Deleted, - () => DataTypeService.Deleted -= DataTypeService_Deleted); - Bind(() => DataTypeService.Saved += DataTypeService_Saved, - () => DataTypeService.Saved -= DataTypeService_Saved); - // bind to stylesheet events Bind(() => FileService.SavedStylesheet += FileService_SavedStylesheet, () => FileService.SavedStylesheet -= FileService_SavedStylesheet); @@ -191,15 +187,15 @@ namespace Umbraco.Cms.Core.Cache #region DataTypeService - private void DataTypeService_Saved(IDataTypeService sender, SaveEventArgs e) + public void Handle(DataTypeSavedNotification notification) { - foreach (var entity in e.SavedEntities) + foreach (var entity in notification.SavedEntities) _distributedCache.RefreshDataTypeCache(entity); } - private void DataTypeService_Deleted(IDataTypeService sender, DeleteEventArgs e) + public void Handle(DataTypeDeletedNotification notification) { - foreach (var entity in e.DeletedEntities) + foreach (var entity in notification.DeletedEntities) _distributedCache.RemoveDataTypeCache(entity); } diff --git a/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs b/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs index 2d9bcf2c9d..80488e91b1 100644 --- a/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs +++ b/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs @@ -42,9 +42,6 @@ namespace Umbraco.Cms.Tests.Integration.Cache var definitions = new IEventDefinition[] { - new EventDefinition>(null, DataTypeService, new SaveEventArgs(Enumerable.Empty())), - new EventDefinition>(null, DataTypeService, new DeleteEventArgs(Enumerable.Empty())), - new EventDefinition>(null, FileService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, FileService, new DeleteEventArgs(Enumerable.Empty())), From 84d98f48c79489c82f611235e00bc37f602a3267 Mon Sep 17 00:00:00 2001 From: Mole Date: Tue, 30 Mar 2021 11:14:50 +0200 Subject: [PATCH 70/78] Register notifications for DistributedCacheBinder --- .../Cache/DistributedCacheBinder_Handlers.cs | 8 ++++++-- .../Compose/NotificationsComposer.cs | 6 +++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs index a6b0dd73db..41195be9fe 100644 --- a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs +++ b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs @@ -189,14 +189,18 @@ namespace Umbraco.Cms.Core.Cache public void Handle(DataTypeSavedNotification notification) { - foreach (var entity in notification.SavedEntities) + foreach (IDataType entity in notification.SavedEntities) + { _distributedCache.RefreshDataTypeCache(entity); + } } public void Handle(DataTypeDeletedNotification notification) { - foreach (var entity in notification.DeletedEntities) + foreach (IDataType entity in notification.DeletedEntities) + { _distributedCache.RemoveDataTypeCache(entity); + } } #endregion diff --git a/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs b/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs index be6539ac55..e9e7bf30af 100644 --- a/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs +++ b/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs @@ -78,7 +78,11 @@ namespace Umbraco.Cms.Core.Compose .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() - .AddNotificationHandler(); + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler(); // add notification handlers for auditing builder From c881fa9e7d08c11954e18489827f70cdafceb947 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 30 Mar 2021 17:03:56 +0200 Subject: [PATCH 71/78] Fixes tabbing-mode remains active after closing modal #9790 (#10074) --- .../components/forms/umbfocuslock.directive.js | 4 ---- .../src/less/application/umb-outline.less | 1 + src/Umbraco.Web.UI.Client/src/less/forms.less | 9 ++++++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js index 569f49b88a..f7cd32217e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js @@ -29,10 +29,6 @@ var defaultFocusedElement = getAutoFocusElement(focusableElements); var firstFocusableElement = focusableElements[0]; var lastFocusableElement = focusableElements[focusableElements.length -1]; - - // We need to add the tabbing-active class in order to highlight the focused button since the default style is - // outline: none; set in the stylesheet specifically - bodyElement.classList.add('tabbing-active'); // If there is no default focused element put focus on the first focusable element in the nodelist if(defaultFocusedElement === null ){ diff --git a/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less b/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less index 939366d5ac..1f1c2c0e72 100644 --- a/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less +++ b/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less @@ -15,6 +15,7 @@ right: 0; border-radius: 3px; box-shadow: 0 0 2px 0px @ui-outline, inset 0 0 2px 2px @ui-outline; + pointer-events: none; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/forms.less b/src/Umbraco.Web.UI.Client/src/less/forms.less index 90b2dbe37e..17c62037cc 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms.less @@ -308,7 +308,14 @@ select[size] { input[type="file"], input[type="radio"], input[type="checkbox"] { - .umb-outline(); + &:focus { + border-color: @inputBorderFocus; + outline: 0; + + .tabbing-active & { + outline: 2px solid @ui-outline; + } + } } From 15923aafc7723f1823a045b7e635cfb4cffbd786 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 30 Mar 2021 17:03:56 +0200 Subject: [PATCH 72/78] Fixes tabbing-mode remains active after closing modal #9790 (#10074) (cherry picked from commit c881fa9e7d08c11954e18489827f70cdafceb947) --- .../components/forms/umbfocuslock.directive.js | 4 ---- .../src/less/application/umb-outline.less | 1 + src/Umbraco.Web.UI.Client/src/less/forms.less | 9 ++++++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js index 569f49b88a..f7cd32217e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js @@ -29,10 +29,6 @@ var defaultFocusedElement = getAutoFocusElement(focusableElements); var firstFocusableElement = focusableElements[0]; var lastFocusableElement = focusableElements[focusableElements.length -1]; - - // We need to add the tabbing-active class in order to highlight the focused button since the default style is - // outline: none; set in the stylesheet specifically - bodyElement.classList.add('tabbing-active'); // If there is no default focused element put focus on the first focusable element in the nodelist if(defaultFocusedElement === null ){ diff --git a/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less b/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less index 939366d5ac..1f1c2c0e72 100644 --- a/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less +++ b/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less @@ -15,6 +15,7 @@ right: 0; border-radius: 3px; box-shadow: 0 0 2px 0px @ui-outline, inset 0 0 2px 2px @ui-outline; + pointer-events: none; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/forms.less b/src/Umbraco.Web.UI.Client/src/less/forms.less index 90b2dbe37e..17c62037cc 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms.less @@ -308,7 +308,14 @@ select[size] { input[type="file"], input[type="radio"], input[type="checkbox"] { - .umb-outline(); + &:focus { + border-color: @inputBorderFocus; + outline: 0; + + .tabbing-active & { + outline: 2px solid @ui-outline; + } + } } From 72e468428ad0c76ea4076d22693cc38f8a331eae Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Fri, 19 Mar 2021 16:57:40 +0100 Subject: [PATCH 73/78] Null check on scope and options to ensure backward compatibility (cherry picked from commit fe8cd239d2f4c528c1a8a3cf4c50e90bb43cacfc) --- .../src/common/services/listviewhelper.service.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js index 14643dc9cd..f9ebba00ea 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js @@ -572,13 +572,15 @@ * Method for opening an item in a list view for editing. * * @param {Object} item The item to edit + * @param {Object} scope The scope with options */ function editItem(item, scope) { + if (!item.editPath) { return; } - if (scope.options.useInfiniteEditor) + if (scope && scope.options && scope.options.useInfiniteEditor) { var editorModel = { id: item.id, From 9381c2e915472cc21bd5d68f06348cdd67744e76 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 31 Mar 2021 19:45:48 +0200 Subject: [PATCH 74/78] Added internals visible to configuration for Umbraco Forms. --- src/Umbraco.Core/Umbraco.Core.csproj | 22 ++++++++++++++++++- .../Umbraco.Infrastructure.csproj | 20 +++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index ce524a09a1..36b9c4d171 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 @@ -46,6 +46,26 @@ <_Parameter1>DynamicProxyGenAssembly2 + + + + <_Parameter1>Umbraco.Forms.Core + + + <_Parameter1>Umbraco.Forms.Core.Providers + + + <_Parameter1>Umbraco.Forms.Web + + + <_Parameter1>Umbraco.Forms.Core.V9 + + + <_Parameter1>Umbraco.Forms.Core.Providers.V9 + + + <_Parameter1>Umbraco.Forms.Web.V9 + diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index e5ee719a05..e870b02049 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -101,6 +101,26 @@ <_Parameter1>DynamicProxyGenAssembly2 + + + + <_Parameter1>Umbraco.Forms.Core + + + <_Parameter1>Umbraco.Forms.Core.Providers + + + <_Parameter1>Umbraco.Forms.Web + + + <_Parameter1>Umbraco.Forms.Core.V9 + + + <_Parameter1>Umbraco.Forms.Core.Providers.V9 + + + <_Parameter1>Umbraco.Forms.Web.V9 + From 1d9dbff72e05f8603ffc0ae0564515488728a77b Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 31 Mar 2021 19:49:54 +0200 Subject: [PATCH 75/78] Removed unnecessary V9 suffixes. --- src/Umbraco.Core/Umbraco.Core.csproj | 29 +++++++------------ .../Umbraco.Infrastructure.csproj | 11 +------ 2 files changed, 11 insertions(+), 29 deletions(-) diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 36b9c4d171..cade1041ac 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -47,25 +47,16 @@ <_Parameter1>DynamicProxyGenAssembly2 - - - <_Parameter1>Umbraco.Forms.Core - - - <_Parameter1>Umbraco.Forms.Core.Providers - - - <_Parameter1>Umbraco.Forms.Web - - - <_Parameter1>Umbraco.Forms.Core.V9 - - - <_Parameter1>Umbraco.Forms.Core.Providers.V9 - - - <_Parameter1>Umbraco.Forms.Web.V9 - + + + <_Parameter1>Umbraco.Forms.Core + + + <_Parameter1>Umbraco.Forms.Core.Providers + + + <_Parameter1>Umbraco.Forms.Web + diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index e870b02049..baa69dddb9 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -102,7 +102,7 @@ <_Parameter1>DynamicProxyGenAssembly2 - + <_Parameter1>Umbraco.Forms.Core @@ -112,15 +112,6 @@ <_Parameter1>Umbraco.Forms.Web - - <_Parameter1>Umbraco.Forms.Core.V9 - - - <_Parameter1>Umbraco.Forms.Core.Providers.V9 - - - <_Parameter1>Umbraco.Forms.Web.V9 - From aae7fc955f7241c89ddfadbb8adfb092f784d79e Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 2 Apr 2021 11:48:21 +0200 Subject: [PATCH 76/78] Made web layer internals visible to Umbraco Forms assemblies. --- src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj | 7 ++++++- src/Umbraco.Web.Common/Umbraco.Web.Common.csproj | 5 +++++ src/Umbraco.Web.Website/Umbraco.Web.Website.csproj | 7 ++++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj index d49eb6e4f5..967e2043f4 100644 --- a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj +++ b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj @@ -1,4 +1,4 @@ - + net5.0 @@ -31,6 +31,11 @@ <_Parameter1>Umbraco.Tests.Integration + + + + <_Parameter1>Umbraco.Forms.Web + diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index bb2ecbc346..24f20f2f4a 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -40,6 +40,11 @@ <_Parameter1>Umbraco.Tests.UnitTests + + + + <_Parameter1>Umbraco.Forms.Web + diff --git a/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj b/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj index a7c5e7a277..31a2ef25b2 100644 --- a/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj +++ b/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj @@ -1,4 +1,4 @@ - + net5.0 @@ -34,5 +34,10 @@ <_Parameter1>Umbraco.Tests.Integration + + + + <_Parameter1>Umbraco.Forms.Web + From 32e1b8361f5ace781ab8146067d024dbebbf16c1 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 6 Apr 2021 08:23:07 +0200 Subject: [PATCH 77/78] Fix for members build issue --- .../DependencyInjection/ServiceCollectionExtensions.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs index 5182db4e20..49ed441932 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs @@ -4,7 +4,6 @@ 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; @@ -12,6 +11,7 @@ using SixLabors.ImageSharp.Web.DependencyInjection; using SixLabors.ImageSharp.Web.Processors; using SixLabors.ImageSharp.Web.Providers; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Web.Common.Security; @@ -69,8 +69,8 @@ namespace Umbraco.Extensions .AddMemberManager() .AddUserStore() .AddRoleStore() - .AddRoleValidator>() - .AddRoleManager>(); + .AddRoleValidator>() + .AddRoleManager>(); private static MemberIdentityBuilder BuildMembersIdentity(this IServiceCollection services) { @@ -78,7 +78,7 @@ namespace Umbraco.Extensions services.TryAddScoped, UserValidator>(); services.TryAddScoped, PasswordValidator>(); services.TryAddScoped, PasswordHasher>(); - return new MemberIdentityBuilder(typeof(IdentityRole), services); + return new MemberIdentityBuilder(typeof(UmbracoIdentityRole), services); } private static void RemoveIntParamenterIfValueGreatherThen(IDictionary commands, string parameter, int maxValue) From 18be3473331c064d311ee7808b733a898a9bfe68 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 6 Apr 2021 18:46:38 +0200 Subject: [PATCH 78/78] Fix todo by checking explicitly for Umbraco Core assembly names instead of "Umbraco.", to avoid special case forms, deploy etc. --- .../Composing/DefaultUmbracoAssemblyProvider.cs | 12 +----------- src/Umbraco.Core/Composing/ReferenceResolver.cs | 5 ++--- src/Umbraco.Core/Constants-Composing.cs | 13 ++++++++++++- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs b/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs index 516f26774a..60aa666034 100644 --- a/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs +++ b/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs @@ -16,16 +16,6 @@ namespace Umbraco.Cms.Core.Composing { private readonly Assembly _entryPointAssembly; private readonly ILoggerFactory _loggerFactory; - private static readonly string[] UmbracoCoreAssemblyNames = new[] - { - "Umbraco.Core", - "Umbraco.Infrastructure", - "Umbraco.PublishedCache.NuCache", - "Umbraco.Examine.Lucene", - "Umbraco.Web.Common", - "Umbraco.Web.BackOffice", - "Umbraco.Web.Website", - }; public DefaultUmbracoAssemblyProvider(Assembly entryPointAssembly, ILoggerFactory loggerFactory) { @@ -43,7 +33,7 @@ namespace Umbraco.Cms.Core.Composing { get { - var finder = new FindAssembliesWithReferencesTo(new[] { _entryPointAssembly }, UmbracoCoreAssemblyNames, true, _loggerFactory); + var finder = new FindAssembliesWithReferencesTo(new[] { _entryPointAssembly }, Constants.Composing.UmbracoCoreAssemblyNames, true, _loggerFactory); return finder.Find(); } } diff --git a/src/Umbraco.Core/Composing/ReferenceResolver.cs b/src/Umbraco.Core/Composing/ReferenceResolver.cs index 6ecd425ac1..fdc72a183b 100644 --- a/src/Umbraco.Core/Composing/ReferenceResolver.cs +++ b/src/Umbraco.Core/Composing/ReferenceResolver.cs @@ -82,9 +82,8 @@ namespace Umbraco.Cms.Core.Composing assemblyName.FullName.StartsWith(f, StringComparison.InvariantCultureIgnoreCase))) continue; - // don't include this item if it's Umbraco - // TODO: We should maybe pass an explicit list of these names in? - if (assemblyName.FullName.StartsWith("Umbraco.") || assemblyName.Name.EndsWith(".Views")) + // don't include this item if it's Umbraco Core + if (Constants.Composing.UmbracoCoreAssemblyNames.Any(x=>assemblyName.FullName.StartsWith(x) || assemblyName.Name.EndsWith(".Views"))) continue; var assembly = Assembly.Load(assemblyName); diff --git a/src/Umbraco.Core/Constants-Composing.cs b/src/Umbraco.Core/Constants-Composing.cs index a92f71ee04..747a74b8d8 100644 --- a/src/Umbraco.Core/Constants-Composing.cs +++ b/src/Umbraco.Core/Constants-Composing.cs @@ -9,6 +9,17 @@ /// Defines constants for composition. /// public static class Composing - { } + { + public static readonly string[] UmbracoCoreAssemblyNames = new[] + { + "Umbraco.Core", + "Umbraco.Infrastructure", + "Umbraco.PublishedCache.NuCache", + "Umbraco.Examine.Lucene", + "Umbraco.Web.Common", + "Umbraco.Web.BackOffice", + "Umbraco.Web.Website", + }; + } } }