From e2b5c2835dfca11fcfbe0eedeea2a868be6f7e97 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 12 Apr 2021 19:23:23 +1000 Subject: [PATCH 01/26] Getting new netcore PublicAccessChecker in place --- src/Umbraco.Core/Routing/IPublishedRouter.cs | 2 +- src/Umbraco.Core/Routing/PublishedRouter.cs | 14 +-- .../Security/IPublicAccessChecker.cs | 4 +- .../Security/MemberRoleStore.cs | 2 +- .../Security/PublicAccessCheckerTests.cs | 66 ++++++++++ .../UmbracoRouteValueTransformerTests.cs | 2 +- .../Routing/UmbracoRouteValuesFactoryTests.cs | 15 +-- .../MembersMembershipProviderTests.cs | 114 ------------------ src/Umbraco.Tests/Testing/UmbracoTestBase.cs | 2 +- src/Umbraco.Tests/Umbraco.Tests.csproj | 1 - .../UmbracoBuilderExtensions.cs | 1 - .../Routing/PublicAccessChecker.cs | 10 -- .../Security/PublicAccessChecker.cs | 59 +++++++++ .../Routing/IUmbracoRouteValuesFactory.cs | 3 +- .../Routing/UmbracoRouteValueTransformer.cs | 2 +- .../Routing/UmbracoRouteValuesFactory.cs | 11 +- .../Security/PublicAccessChecker.cs | 60 --------- src/Umbraco.Web/Umbraco.Web.csproj | 35 +++--- 18 files changed, 173 insertions(+), 230 deletions(-) create mode 100644 src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/PublicAccessCheckerTests.cs delete mode 100644 src/Umbraco.Tests/Membership/MembersMembershipProviderTests.cs delete mode 100644 src/Umbraco.Web.Common/Routing/PublicAccessChecker.cs create mode 100644 src/Umbraco.Web.Common/Security/PublicAccessChecker.cs delete mode 100644 src/Umbraco.Web/Security/PublicAccessChecker.cs diff --git a/src/Umbraco.Core/Routing/IPublishedRouter.cs b/src/Umbraco.Core/Routing/IPublishedRouter.cs index 39bc94cda1b5..c035a9b85b37 100644 --- a/src/Umbraco.Core/Routing/IPublishedRouter.cs +++ b/src/Umbraco.Core/Routing/IPublishedRouter.cs @@ -36,6 +36,6 @@ public interface IPublishedRouter /// In that case it's the same as if there was no content which means even if there was /// content matched we want to run the request through the last chance finders. /// - IPublishedRequestBuilder UpdateRequestToNotFound(IPublishedRequest request); + Task UpdateRequestToNotFoundAsync(IPublishedRequest request); } } diff --git a/src/Umbraco.Core/Routing/PublishedRouter.cs b/src/Umbraco.Core/Routing/PublishedRouter.cs index 86ac97db310f..6b66862f352a 100644 --- a/src/Umbraco.Core/Routing/PublishedRouter.cs +++ b/src/Umbraco.Core/Routing/PublishedRouter.cs @@ -209,7 +209,7 @@ internal IPublishedRequest BuildRequest(IPublishedRequestBuilder frequest) } /// - public IPublishedRequestBuilder UpdateRequestToNotFound(IPublishedRequest request) + public async Task UpdateRequestToNotFoundAsync(IPublishedRequest request) { var builder = new PublishedRequestBuilder(request.Uri, _fileService); @@ -217,7 +217,7 @@ public IPublishedRequestBuilder UpdateRequestToNotFound(IPublishedRequest reques IPublishedContent content = request.PublishedContent; builder.SetPublishedContent(null); - HandlePublishedContent(builder); // will go 404 + await HandlePublishedContentAsync(builder); // will go 404 FindTemplate(builder, false); // if request has been flagged to redirect then return @@ -383,7 +383,7 @@ private void FindPublishedContentAndTemplate(IPublishedRequestBuilder request) // so internal redirect, 404, etc has precedence over redirect // handle not-found, redirects, access... - HandlePublishedContent(request); + HandlePublishedContentAsync(request); // find a template FindTemplate(request, foundContentByFinders); @@ -433,7 +433,7 @@ internal void FindPublishedContent(IPublishedRequestBuilder request) /// Handles "not found", internal redirects, access validation... /// things that must be handled in one place because they can create loops /// - private void HandlePublishedContent(IPublishedRequestBuilder request) + private async Task HandlePublishedContentAsync(IPublishedRequestBuilder request) { // because these might loop, we have to have some sort of infinite loop detection int i = 0, j = 0; @@ -472,7 +472,7 @@ private void HandlePublishedContent(IPublishedRequestBuilder request) // ensure access if (request.PublishedContent != null) { - EnsurePublishedContentAccess(request); + await EnsurePublishedContentAccess(request); } // loop while we don't have page, ie the redirect or access @@ -577,7 +577,7 @@ private bool FollowInternalRedirects(IPublishedRequestBuilder request) /// Ensures that access to current node is permitted. /// /// Redirecting to a different site root and/or culture will not pick the new site root nor the new culture. - private void EnsurePublishedContentAccess(IPublishedRequestBuilder request) + private async Task EnsurePublishedContentAccess(IPublishedRequestBuilder request) { if (request.PublishedContent == null) { @@ -592,7 +592,7 @@ private void EnsurePublishedContentAccess(IPublishedRequestBuilder request) { _logger.LogDebug("EnsurePublishedContentAccess: Page is protected, check for access"); - PublicAccessStatus status = _publicAccessChecker.HasMemberAccessToContent(request.PublishedContent.Id); + PublicAccessStatus status = await _publicAccessChecker.HasMemberAccessToContentAsync(request.PublishedContent.Id); switch (status) { case PublicAccessStatus.NotLoggedIn: diff --git a/src/Umbraco.Core/Security/IPublicAccessChecker.cs b/src/Umbraco.Core/Security/IPublicAccessChecker.cs index 4eaa184f5a68..6ec9eb7ade61 100644 --- a/src/Umbraco.Core/Security/IPublicAccessChecker.cs +++ b/src/Umbraco.Core/Security/IPublicAccessChecker.cs @@ -1,7 +1,9 @@ +using System.Threading.Tasks; + namespace Umbraco.Cms.Core.Security { public interface IPublicAccessChecker { - PublicAccessStatus HasMemberAccessToContent(int publishedContentId); + Task HasMemberAccessToContentAsync(int publishedContentId); } } diff --git a/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs b/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs index 279735bfa20d..da4db4b60644 100644 --- a/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs @@ -20,7 +20,7 @@ public class MemberRoleStore : IRoleStore //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"; + //private const string genericIdentityErrorCode = "IdentityErrorUserStore"; public MemberRoleStore(IMemberGroupService memberGroupService, IdentityErrorDescriber errorDescriber) { diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/PublicAccessCheckerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/PublicAccessCheckerTests.cs new file mode 100644 index 000000000000..232add667a26 --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/PublicAccessCheckerTests.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Security.Principal; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using NUnit.Framework; +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 +{ + [TestFixture] + public class PublicAccessCheckerTests + { + private PublicAccessChecker CreateSut(out HttpContext httpContext, out IMemberManager memberManager) + { + memberManager = Mock.Of(); + IPublicAccessService publicAccessService = Mock.Of(); + IContentService contentService = Mock.Of(); + + var services = new ServiceCollection(); + IMemberManager localMemberManager = memberManager; + services.AddScoped(x => localMemberManager); + httpContext = new DefaultHttpContext + { + RequestServices = services.BuildServiceProvider() + }; + + HttpContext localHttpContext = httpContext; + var publicAccessChecker = new PublicAccessChecker( + Mock.Of(x => x.HttpContext == localHttpContext), + publicAccessService, + contentService); + + return publicAccessChecker; + } + + [Test] + public async Task GivenMemberNotLoggedIn_WhenIdentityIsChecked_ThenNotLoggedInResponse() + { + PublicAccessChecker sut = CreateSut(out HttpContext httpContext, out IMemberManager memberManager); + httpContext.User = new ClaimsPrincipal(); + Mock.Get(memberManager).Setup(x => x.GetUserAsync(It.IsAny())).Returns(Task.FromResult(new MemberIdentityUser())); + + var result = await sut.HasMemberAccessToContentAsync(123); + Assert.AreEqual(PublicAccessStatus.NotLoggedIn, result); + } + + [Test] + public async Task GivenMemberNotLoggedIn_WhenMemberIsRequested_AndIsNull_ThenNotLoggedInResponse() + { + PublicAccessChecker sut = CreateSut(out HttpContext httpContext, out IMemberManager memberManager); + httpContext.User = new ClaimsPrincipal(Mock.Of(x => x.IsAuthenticated == true)); + Mock.Get(memberManager).Setup(x => x.GetUserAsync(It.IsAny())).Returns(Task.FromResult((MemberIdentityUser)null)); + + var result = await sut.HasMemberAccessToContentAsync(123); + Assert.AreEqual(PublicAccessStatus.NotLoggedIn, result); + } + } +} diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformerTests.cs index e8e8fec2e0bc..390f60e5f5b1 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformerTests.cs @@ -86,7 +86,7 @@ private UmbracoRouteValues GetRouteValues(IPublishedRequest request) }); private IUmbracoRouteValuesFactory GetRouteValuesFactory(IPublishedRequest request) - => Mock.Of(x => x.Create(It.IsAny(), It.IsAny()) == GetRouteValues(request)); + => Mock.Of(x => x.CreateAsync(It.IsAny(), It.IsAny()) == Task.FromResult(GetRouteValues(request))); private IPublishedRouter GetRouter(IPublishedRequest request) => Mock.Of(x => x.RouteRequestAsync(It.IsAny(), It.IsAny()) == Task.FromResult(request)); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactoryTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactoryTests.cs index 54c9fd943837..dc8ec7b6ff66 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactoryTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactoryTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Reflection; +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Controllers; @@ -36,8 +37,8 @@ private UmbracoRouteValuesFactory GetFactory( request = builder.Build(); publishedRouter = new Mock(); - publishedRouter.Setup(x => x.UpdateRequestToNotFound(It.IsAny())) - .Returns((IPublishedRequest r) => builder) + publishedRouter.Setup(x => x.UpdateRequestToNotFoundAsync(It.IsAny())) + .Returns((IPublishedRequest r) => Task.FromResult((IPublishedRequestBuilder)builder)) .Verifiable(); renderingDefaults = new UmbracoRenderingDefaults(); @@ -68,22 +69,22 @@ private UmbracoRouteValuesFactory GetFactory( } [Test] - public void Update_Request_To_Not_Found_When_No_Template() + public async Task Update_Request_To_Not_Found_When_No_Template() { UmbracoRouteValuesFactory factory = GetFactory(out Mock publishedRouter, out _, out IPublishedRequest request); - UmbracoRouteValues result = factory.Create(new DefaultHttpContext(), request); + UmbracoRouteValues result = await factory.CreateAsync(new DefaultHttpContext(), request); // The request has content, no template, no hijacked route and no disabled template features so UpdateRequestToNotFound will be called - publishedRouter.Verify(m => m.UpdateRequestToNotFound(It.IsAny()), Times.Once); + publishedRouter.Verify(m => m.UpdateRequestToNotFoundAsync(It.IsAny()), Times.Once); } [Test] - public void Adds_Result_To_Route_Value_Dictionary() + public async Task Adds_Result_To_Route_Value_Dictionary() { UmbracoRouteValuesFactory factory = GetFactory(out _, out UmbracoRenderingDefaults renderingDefaults, out IPublishedRequest request); - UmbracoRouteValues result = factory.Create(new DefaultHttpContext(), request); + UmbracoRouteValues result = await factory.CreateAsync(new DefaultHttpContext(), request); Assert.IsNotNull(result); Assert.AreEqual(renderingDefaults.DefaultControllerType, result.ControllerType); diff --git a/src/Umbraco.Tests/Membership/MembersMembershipProviderTests.cs b/src/Umbraco.Tests/Membership/MembersMembershipProviderTests.cs deleted file mode 100644 index 8f1e36aa1110..000000000000 --- a/src/Umbraco.Tests/Membership/MembersMembershipProviderTests.cs +++ /dev/null @@ -1,114 +0,0 @@ -// using System.Collections.Specialized; -// using System.Web.Security; -// using Moq; -// using NUnit.Framework; -// using Umbraco.Core; -// using Umbraco.Core.Cache; -// using Umbraco.Core.Composing; -// using Umbraco.Core.Logging; -// using Umbraco.Core.Models; -// using Umbraco.Core.Services; -// using Umbraco.Core.Sync; -// using Umbraco.Tests.Integration; -// using Umbraco.Tests.TestHelpers; -// using Umbraco.Tests.TestHelpers.Entities; -// using Umbraco.Tests.Testing; -// using Umbraco.Web; -// using Umbraco.Web.Cache; -// using Umbraco.Web.Security.Providers; -// -// namespace Umbraco.Tests.Membership -// { -// [TestFixture] -// [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)] -// public class MembersMembershipProviderTests : TestWithDatabaseBase -// { -// private MembersMembershipProvider MembersMembershipProvider { get; set; } -// private IDistributedCacheBinder DistributedCacheBinder { get; set; } -// -// public IMemberService MemberService => Current.Factory.GetInstance(); -// public IMemberTypeService MemberTypeService => Current.Factory.GetInstance(); -// public ILogger Logger => Current.Factory.GetInstance(); -// -// public override void SetUp() -// { -// base.SetUp(); -// -// MembersMembershipProvider = new MembersMembershipProvider(MemberService, MemberTypeService); -// -// MembersMembershipProvider.Initialize("test", new NameValueCollection { { "passwordFormat", MembershipPasswordFormat.Clear.ToString() } }); -// -// DistributedCacheBinder = new DistributedCacheBinder(new DistributedCache(), Mock.Of(), Logger); -// DistributedCacheBinder.BindEvents(true); -// } -// -// [TearDown] -// public void Teardown() -// { -// DistributedCacheBinder?.UnbindEvents(); -// DistributedCacheBinder = null; -// } -// -// protected override void Compose() -// { -// base.Compose(); -// -// // the cache refresher component needs to trigger to refresh caches -// // but then, it requires a lot of plumbing ;( -// // FIXME: and we cannot inject a DistributedCache yet -// // so doing all this mess -// Composition.RegisterUnique(); -// Composition.RegisterUnique(f => Mock.Of()); -// Composition.WithCollectionBuilder() -// .Add(() => Composition.TypeLoader.GetCacheRefreshers()); -// } -// -// protected override AppCaches GetAppCaches() -// { -// // this is what's created core web runtime -// return new AppCaches( -// new DeepCloneAppCache(new ObjectCacheAppCache()), -// NoAppCache.Instance, -// new IsolatedCaches(type => new DeepCloneAppCache(new ObjectCacheAppCache()))); -// } -// -// /// -// /// MembersMembershipProvider.ValidateUser is expected to increase the number of failed attempts and also read that same number. -// /// -// /// -// /// This test requires the caching to be enabled, as it already is correct in the database. -// /// Shows the error described here: https://github.com/umbraco/Umbraco-CMS/issues/9861 -// /// -// [Test] -// public void ValidateUser__must_lock_out_users_after_max_attempts_of_wrong_password() -// { -// // Arrange -// IMemberType memberType = MockedContentTypes.CreateSimpleMemberType(); -// ServiceContext.MemberTypeService.Save(memberType); -// var member = MockedMember.CreateSimpleMember(memberType, "test", "test@test.com", "password","test"); -// ServiceContext.MemberService.Save(member); -// -// var wrongPassword = "wrongPassword"; -// var numberOfFailedAttempts = MembersMembershipProvider.MaxInvalidPasswordAttempts+2; -// -// // Act -// var memberBefore = ServiceContext.MemberService.GetById(member.Id); -// for (int i = 0; i < numberOfFailedAttempts; i++) -// { -// MembersMembershipProvider.ValidateUser(member.Username, wrongPassword); -// } -// var memberAfter = ServiceContext.MemberService.GetById(member.Id); -// -// // Assert -// Assert.Multiple(() => -// { -// Assert.AreEqual(0 , memberBefore.FailedPasswordAttempts, "Expected 0 failed password attempts before"); -// Assert.IsFalse(memberBefore.IsLockedOut, "Expected the member NOT to be locked out before"); -// -// Assert.AreEqual(MembersMembershipProvider.MaxInvalidPasswordAttempts, memberAfter.FailedPasswordAttempts, "Expected exactly the max possible failed password attempts after"); -// Assert.IsTrue(memberAfter.IsLockedOut, "Expected the member to be locked out after"); -// }); -// -// } -// } -// } diff --git a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs index 15634c608373..552f93ed74ee 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs @@ -228,7 +228,7 @@ public virtual void SetUp() services.AddUnique(ipResolver); services.AddUnique(); services.AddUnique(TestHelper.ShortStringHelper); - services.AddUnique(); + //services.AddUnique(); var memberService = Mock.Of(); diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 1bce8edcaccc..32617b4d8ba5 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -157,7 +157,6 @@ - diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 106fce4b596f..d34f7f373082 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -54,7 +54,6 @@ using Umbraco.Cms.Web.Common.ModelBinders; using Umbraco.Cms.Web.Common.Mvc; using Umbraco.Cms.Web.Common.Profiler; -using Umbraco.Cms.Web.Common.Routing; using Umbraco.Cms.Web.Common.RuntimeMinification; using Umbraco.Cms.Web.Common.Security; using Umbraco.Cms.Web.Common.Templates; diff --git a/src/Umbraco.Web.Common/Routing/PublicAccessChecker.cs b/src/Umbraco.Web.Common/Routing/PublicAccessChecker.cs deleted file mode 100644 index 0ac3125d879d..000000000000 --- a/src/Umbraco.Web.Common/Routing/PublicAccessChecker.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Umbraco.Cms.Core.Security; - -namespace Umbraco.Cms.Web.Common.Routing -{ - public class PublicAccessChecker : IPublicAccessChecker - { - //TODO implement - public PublicAccessStatus HasMemberAccessToContent(int publishedContentId) => PublicAccessStatus.AccessAccepted; - } -} diff --git a/src/Umbraco.Web.Common/Security/PublicAccessChecker.cs b/src/Umbraco.Web.Common/Security/PublicAccessChecker.cs new file mode 100644 index 000000000000..b7fdc4f1716c --- /dev/null +++ b/src/Umbraco.Web.Common/Security/PublicAccessChecker.cs @@ -0,0 +1,59 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Web.Common.Security +{ + public class PublicAccessChecker : IPublicAccessChecker + { + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IPublicAccessService _publicAccessService; + private readonly IContentService _contentService; + + public PublicAccessChecker(IHttpContextAccessor httpContextAccessor, IPublicAccessService publicAccessService, IContentService contentService) + { + _httpContextAccessor = httpContextAccessor; + _publicAccessService = publicAccessService; + _contentService = contentService; + } + + public async Task HasMemberAccessToContentAsync(int publishedContentId) + { + HttpContext httpContext = _httpContextAccessor.GetRequiredHttpContext(); + IMemberManager memberManager = httpContext.RequestServices.GetRequiredService(); + if (httpContext.User.Identity == null || !httpContext.User.Identity.IsAuthenticated) + { + return PublicAccessStatus.NotLoggedIn; + } + MemberIdentityUser currentMember = await memberManager.GetUserAsync(httpContext.User); + if (currentMember == null) + { + return PublicAccessStatus.NotLoggedIn; + } + + var username = currentMember.UserName; + System.Collections.Generic.IList userRoles = await memberManager.GetRolesAsync(currentMember); + + if (!_publicAccessService.HasAccess(publishedContentId, _contentService, username, userRoles)) + { + return PublicAccessStatus.AccessDenied; + } + + if (!currentMember.IsApproved) + { + return PublicAccessStatus.NotApproved; + } + + if (currentMember.IsLockedOut) + { + return PublicAccessStatus.LockedOut; + } + + return PublicAccessStatus.AccessAccepted; + } + } +} diff --git a/src/Umbraco.Web.Website/Routing/IUmbracoRouteValuesFactory.cs b/src/Umbraco.Web.Website/Routing/IUmbracoRouteValuesFactory.cs index 7e30773bf5fe..e25921bd9163 100644 --- a/src/Umbraco.Web.Website/Routing/IUmbracoRouteValuesFactory.cs +++ b/src/Umbraco.Web.Website/Routing/IUmbracoRouteValuesFactory.cs @@ -1,3 +1,4 @@ +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Web.Common.Routing; @@ -12,6 +13,6 @@ public interface IUmbracoRouteValuesFactory /// /// Creates /// - UmbracoRouteValues Create(HttpContext httpContext, IPublishedRequest request); + Task CreateAsync(HttpContext httpContext, IPublishedRequest request); } } diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs index b8446ce718bd..4c42f67be589 100644 --- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs @@ -116,7 +116,7 @@ public override async ValueTask TransformAsync(HttpContext IPublishedRequest publishedRequest = await RouteRequestAsync(_umbracoContextAccessor.UmbracoContext); - UmbracoRouteValues umbracoRouteValues = _routeValuesFactory.Create(httpContext, publishedRequest); + UmbracoRouteValues umbracoRouteValues = await _routeValuesFactory.CreateAsync(httpContext, publishedRequest); // Store the route values as a httpcontext feature httpContext.Features.Set(umbracoRouteValues); diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactory.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactory.cs index f44890cf2f10..0c220afb304a 100644 --- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactory.cs +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactory.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Controllers; using Umbraco.Cms.Core.Features; @@ -63,7 +64,7 @@ public UmbracoRouteValuesFactory( protected string DefaultControllerName => _defaultControllerName.Value; /// - public UmbracoRouteValues Create(HttpContext httpContext, IPublishedRequest request) + public async Task CreateAsync(HttpContext httpContext, IPublishedRequest request) { if (httpContext is null) { @@ -95,7 +96,7 @@ public UmbracoRouteValues Create(HttpContext httpContext, IPublishedRequest requ def = CheckHijackedRoute(httpContext, def, out bool hasHijackedRoute); - def = CheckNoTemplate(httpContext, def, hasHijackedRoute); + def = await CheckNoTemplateAsync(httpContext, def, hasHijackedRoute); return def; } @@ -129,7 +130,7 @@ private UmbracoRouteValues CheckHijackedRoute(HttpContext httpContext, UmbracoRo /// /// Special check for when no template or hijacked route is done which needs to re-run through the routing pipeline again for last chance finders /// - private UmbracoRouteValues CheckNoTemplate(HttpContext httpContext, UmbracoRouteValues def, bool hasHijackedRoute) + private async Task CheckNoTemplateAsync(HttpContext httpContext, UmbracoRouteValues def, bool hasHijackedRoute) { IPublishedRequest request = def.PublishedRequest; @@ -147,11 +148,11 @@ private UmbracoRouteValues CheckNoTemplate(HttpContext httpContext, UmbracoRoute // This is basically a 404 even if there is content found. // We then need to re-run this through the pipeline for the last // chance finders to work. - IPublishedRequestBuilder builder = _publishedRouter.UpdateRequestToNotFound(request); + IPublishedRequestBuilder builder = await _publishedRouter.UpdateRequestToNotFoundAsync(request); if (builder == null) { - throw new InvalidOperationException($"The call to {nameof(IPublishedRouter.UpdateRequestToNotFound)} cannot return null"); + throw new InvalidOperationException($"The call to {nameof(IPublishedRouter.UpdateRequestToNotFoundAsync)} cannot return null"); } request = builder.Build(); diff --git a/src/Umbraco.Web/Security/PublicAccessChecker.cs b/src/Umbraco.Web/Security/PublicAccessChecker.cs deleted file mode 100644 index 8ac0c4be8d12..000000000000 --- a/src/Umbraco.Web/Security/PublicAccessChecker.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.Security; -using Umbraco.Cms.Core.Services; -using Umbraco.Extensions; -using Constants = Umbraco.Cms.Core.Constants; - -namespace Umbraco.Web.Security -{ - public class PublicAccessChecker : IPublicAccessChecker - { - //TODO: This is lazy to avoid circular dependency. We don't care right now because all membership is going to be changed. - private readonly Lazy _membershipHelper; - private readonly IPublicAccessService _publicAccessService; - private readonly IContentService _contentService; - private readonly IPublishedValueFallback _publishedValueFallback; - - public PublicAccessChecker(Lazy membershipHelper, IPublicAccessService publicAccessService, IContentService contentService, IPublishedValueFallback publishedValueFallback) - { - _membershipHelper = membershipHelper; - _publicAccessService = publicAccessService; - _contentService = contentService; - _publishedValueFallback = publishedValueFallback; - } - - public PublicAccessStatus HasMemberAccessToContent(int publishedContentId) - { - var membershipHelper = _membershipHelper.Value; - - if (membershipHelper.IsLoggedIn() == false) - { - return PublicAccessStatus.NotLoggedIn; - } - - var username = membershipHelper.CurrentUserName; - var userRoles = membershipHelper.GetCurrentUserRoles(); - - if (_publicAccessService.HasAccess(publishedContentId, _contentService, username, userRoles) == false) - { - return PublicAccessStatus.AccessDenied; - } - - var member = membershipHelper.GetCurrentMember(); - - if (member.HasProperty(Constants.Conventions.Member.IsApproved) == false) - { - return PublicAccessStatus.NotApproved; - } - - if (member.HasProperty(Constants.Conventions.Member.IsLockedOut) && - member.Value(_publishedValueFallback, Constants.Conventions.Member.IsApproved)) - { - return PublicAccessStatus.LockedOut; - } - - return PublicAccessStatus.AccessAccepted; - } - } -} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index ec1db29f991e..d2b765dc78fa 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -149,7 +149,6 @@ - @@ -238,20 +237,20 @@ Name="UmbGenerateSerializationAssemblies" Condition="'$(_SGenGenerateSerializationAssembliesConfig)' == 'On' or ('@(WebReferenceUrl)'!='' and '$(_SGenGenerateSerializationAssembliesConfig)' == 'Auto')" --> - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + \ No newline at end of file From 8ec0d3f3f3fc3d63ac0180a1cccb10f5a6a49c69 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 13 Apr 2021 11:46:38 +1000 Subject: [PATCH 02/26] Adds full test coverage for PublicAccessChecker --- .../Services/PublicAccessServiceExtensions.cs | 17 +- .../Security/PublicAccessCheckerTests.cs | 226 ++++++++++++++++-- .../Security/MemberManager.cs | 2 +- .../Security/PublicAccessChecker.cs | 10 +- 4 files changed, 236 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs b/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs index 41bf9ac349f6..44e9d61fee8c 100644 --- a/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs +++ b/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; @@ -80,6 +80,21 @@ public static bool HasAccess(this IPublicAccessService publicAccessService, stri private static bool HasAccess(PublicAccessEntry entry, string username, IEnumerable roles) { + if (entry is null) + { + throw new ArgumentNullException(nameof(entry)); + } + + if (string.IsNullOrEmpty(username)) + { + throw new ArgumentException($"'{nameof(username)}' cannot be null or empty.", nameof(username)); + } + + if (roles is null) + { + throw new ArgumentNullException(nameof(roles)); + } + return entry.Rules.Any(x => (x.RuleType == Constants.Conventions.PublicAccess.MemberUsernameRuleType && username.Equals(x.RuleValue, StringComparison.OrdinalIgnoreCase)) || (x.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType && roles.Contains(x.RuleValue)) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/PublicAccessCheckerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/PublicAccessCheckerTests.cs index 232add667a26..52c68b551ff1 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/PublicAccessCheckerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/PublicAccessCheckerTests.cs @@ -9,8 +9,11 @@ using Microsoft.Extensions.DependencyInjection; using Moq; using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.UnitTests.AutoFixture; using Umbraco.Cms.Web.Common.Security; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security @@ -18,49 +21,242 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security [TestFixture] public class PublicAccessCheckerTests { - private PublicAccessChecker CreateSut(out HttpContext httpContext, out IMemberManager memberManager) + private IHttpContextAccessor GetHttpContextAccessor(IMemberManager memberManager, out HttpContext httpContext) { - memberManager = Mock.Of(); - IPublicAccessService publicAccessService = Mock.Of(); - IContentService contentService = Mock.Of(); - var services = new ServiceCollection(); - IMemberManager localMemberManager = memberManager; - services.AddScoped(x => localMemberManager); + services.AddScoped(x => memberManager); httpContext = new DefaultHttpContext { RequestServices = services.BuildServiceProvider() }; HttpContext localHttpContext = httpContext; + return Mock.Of(x => x.HttpContext == localHttpContext); + } + + private PublicAccessChecker CreateSut(IMemberManager memberManager, IPublicAccessService publicAccessService, IContentService contentService, out HttpContext httpContext) + { var publicAccessChecker = new PublicAccessChecker( - Mock.Of(x => x.HttpContext == localHttpContext), + GetHttpContextAccessor(memberManager, out httpContext), publicAccessService, contentService); return publicAccessChecker; } + private ClaimsPrincipal GetLoggedInUser() + { + var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { + new Claim(ClaimTypes.NameIdentifier, "1234"), + new Claim(ClaimTypes.Name, "test@example.com") + }, "TestAuthentication")); + return user; + } + + private void MockGetRolesAsync(IMemberManager memberManager, IEnumerable roles = null) + { + if (roles == null) + { + roles = new[] + { + "role1", + "role2" + }; + } + Mock.Get(memberManager).Setup(x => x.GetRolesAsync(It.IsAny())) + .Returns(Task.FromResult((IList)new List(roles))); + } + + private void MockGetUserAsync(IMemberManager memberManager, MemberIdentityUser memberIdentityUser) + => Mock.Get(memberManager).Setup(x => x.GetUserAsync(It.IsAny())).Returns(Task.FromResult(memberIdentityUser)); + + private PublicAccessEntry GetPublicAccessEntry(string usernameRuleValue, string roleRuleValue) + => new PublicAccessEntry(Guid.NewGuid(), 123, 987, 987, new List + { + new PublicAccessRule + { + RuleType = Constants.Conventions.PublicAccess.MemberUsernameRuleType, + RuleValue = usernameRuleValue + }, + new PublicAccessRule + { + RuleType = Constants.Conventions.PublicAccess.MemberRoleRuleType, + RuleValue = roleRuleValue + } + }); + + [AutoMoqData] [Test] - public async Task GivenMemberNotLoggedIn_WhenIdentityIsChecked_ThenNotLoggedInResponse() + public async Task GivenMemberNotLoggedIn_WhenIdentityIsChecked_ThenNotLoggedInResult( + IMemberManager memberManager, + IPublicAccessService publicAccessService, + IContentService contentService) { - PublicAccessChecker sut = CreateSut(out HttpContext httpContext, out IMemberManager memberManager); + PublicAccessChecker sut = CreateSut(memberManager, publicAccessService, contentService, out HttpContext httpContext); httpContext.User = new ClaimsPrincipal(); - Mock.Get(memberManager).Setup(x => x.GetUserAsync(It.IsAny())).Returns(Task.FromResult(new MemberIdentityUser())); + MockGetUserAsync(memberManager, new MemberIdentityUser()); var result = await sut.HasMemberAccessToContentAsync(123); Assert.AreEqual(PublicAccessStatus.NotLoggedIn, result); } + [AutoMoqData] [Test] - public async Task GivenMemberNotLoggedIn_WhenMemberIsRequested_AndIsNull_ThenNotLoggedInResponse() + public async Task GivenMemberNotLoggedIn_WhenMemberIsRequested_AndIsNull_ThenNotLoggedInResult( + IMemberManager memberManager, + IPublicAccessService publicAccessService, + IContentService contentService) { - PublicAccessChecker sut = CreateSut(out HttpContext httpContext, out IMemberManager memberManager); - httpContext.User = new ClaimsPrincipal(Mock.Of(x => x.IsAuthenticated == true)); - Mock.Get(memberManager).Setup(x => x.GetUserAsync(It.IsAny())).Returns(Task.FromResult((MemberIdentityUser)null)); + PublicAccessChecker sut = CreateSut(memberManager, publicAccessService, contentService, out HttpContext httpContext); + httpContext.User = GetLoggedInUser(); + MockGetUserAsync(memberManager, null); var result = await sut.HasMemberAccessToContentAsync(123); Assert.AreEqual(PublicAccessStatus.NotLoggedIn, result); } + + [AutoMoqData] + [Test] + public async Task GivenMemberLoggedIn_WhenMemberHasNoRoles_ThenAccessDeniedResult( + IMemberManager memberManager, + IPublicAccessService publicAccessService, + IContentService contentService) + { + PublicAccessChecker sut = CreateSut(memberManager, publicAccessService, contentService, out HttpContext httpContext); + + httpContext.User = GetLoggedInUser(); + MockGetUserAsync(memberManager, new MemberIdentityUser()); + MockGetRolesAsync(memberManager, Enumerable.Empty()); + + var result = await sut.HasMemberAccessToContentAsync(123); + Assert.AreEqual(PublicAccessStatus.AccessDenied, result); + } + + [AutoMoqData] + [Test] + public async Task GivenMemberLoggedIn_WhenMemberIsLockedOut_ThenLockedOutResult( + IMemberManager memberManager, + IPublicAccessService publicAccessService, + IContentService contentService) + { + PublicAccessChecker sut = CreateSut(memberManager, publicAccessService, contentService, out HttpContext httpContext); + + httpContext.User = GetLoggedInUser(); + MockGetUserAsync(memberManager, new MemberIdentityUser { IsApproved = true, LockoutEnd = DateTime.UtcNow.AddDays(10) }); + MockGetRolesAsync(memberManager); + + var result = await sut.HasMemberAccessToContentAsync(123); + Assert.AreEqual(PublicAccessStatus.LockedOut, result); + } + + [AutoMoqData] + [Test] + public async Task GivenMemberLoggedIn_WhenMemberIsNotApproved_ThenNotApprovedResult( + IMemberManager memberManager, + IPublicAccessService publicAccessService, + IContentService contentService) + { + PublicAccessChecker sut = CreateSut(memberManager, publicAccessService, contentService, out HttpContext httpContext); + + httpContext.User = GetLoggedInUser(); + MockGetUserAsync(memberManager, new MemberIdentityUser { IsApproved = false }); + MockGetRolesAsync(memberManager); + + var result = await sut.HasMemberAccessToContentAsync(123); + Assert.AreEqual(PublicAccessStatus.NotApproved, result); + } + + [AutoMoqData] + [Test] + public async Task GivenMemberLoggedIn_WhenMemberHasRoles_AndContentDoesNotExist_ThenAccessAcceptedResult( + IMemberManager memberManager, + IPublicAccessService publicAccessService, + IContentService contentService) + { + PublicAccessChecker sut = CreateSut(memberManager, publicAccessService, contentService, out HttpContext httpContext); + httpContext.User = GetLoggedInUser(); + MockGetUserAsync(memberManager, new MemberIdentityUser { IsApproved = true}); + MockGetRolesAsync(memberManager); + Mock.Get(contentService).Setup(x => x.GetById(123)).Returns((IContent)null); + + var result = await sut.HasMemberAccessToContentAsync(123); + Assert.AreEqual(PublicAccessStatus.AccessAccepted, result); + } + + [AutoMoqData] + [Test] + public async Task GivenMemberLoggedIn_WhenMemberHasRoles_AndGetEntryForContentDoesNotExist_ThenAccessAcceptedResult( + IMemberManager memberManager, + IPublicAccessService publicAccessService, + IContentService contentService, + IContent content) + { + PublicAccessChecker sut = CreateSut(memberManager, publicAccessService, contentService, out HttpContext httpContext); + httpContext.User = GetLoggedInUser(); + MockGetUserAsync(memberManager, new MemberIdentityUser { IsApproved = true }); + MockGetRolesAsync(memberManager); + Mock.Get(contentService).Setup(x => x.GetById(123)).Returns(content); + Mock.Get(publicAccessService).Setup(x => x.GetEntryForContent(content)).Returns((PublicAccessEntry)null); + + var result = await sut.HasMemberAccessToContentAsync(123); + Assert.AreEqual(PublicAccessStatus.AccessAccepted, result); + } + + [AutoMoqData] + [Test] + public async Task GivenMemberLoggedIn_WhenMemberHasRoles_AndEntryRulesDontMatch_ThenAccessDeniedResult( + IMemberManager memberManager, + IPublicAccessService publicAccessService, + IContentService contentService, + IContent content) + { + PublicAccessChecker sut = CreateSut(memberManager, publicAccessService, contentService, out HttpContext httpContext); + httpContext.User = GetLoggedInUser(); + MockGetUserAsync(memberManager, new MemberIdentityUser { UserName = "MyUsername", IsApproved = true }); + MockGetRolesAsync(memberManager); + Mock.Get(contentService).Setup(x => x.GetById(123)).Returns(content); + Mock.Get(publicAccessService).Setup(x => x.GetEntryForContent(content)).Returns(GetPublicAccessEntry(string.Empty, string.Empty)); + + var result = await sut.HasMemberAccessToContentAsync(123); + Assert.AreEqual(PublicAccessStatus.AccessDenied, result); + } + + [AutoMoqData] + [Test] + public async Task GivenMemberLoggedIn_WhenMemberHasRoles_AndUsernameRuleMatches_ThenAccessAcceptedResult( + IMemberManager memberManager, + IPublicAccessService publicAccessService, + IContentService contentService, + IContent content) + { + PublicAccessChecker sut = CreateSut(memberManager, publicAccessService, contentService, out HttpContext httpContext); + httpContext.User = GetLoggedInUser(); + MockGetUserAsync(memberManager, new MemberIdentityUser { UserName = "MyUsername", IsApproved = true }); + MockGetRolesAsync(memberManager); + Mock.Get(contentService).Setup(x => x.GetById(123)).Returns(content); + Mock.Get(publicAccessService).Setup(x => x.GetEntryForContent(content)).Returns(GetPublicAccessEntry("MyUsername", string.Empty)); + + var result = await sut.HasMemberAccessToContentAsync(123); + Assert.AreEqual(PublicAccessStatus.AccessAccepted, result); + } + + [AutoMoqData] + [Test] + public async Task GivenMemberLoggedIn_WhenMemberHasRoles_AndRoleRuleMatches_ThenAccessAcceptedResult( + IMemberManager memberManager, + IPublicAccessService publicAccessService, + IContentService contentService, + IContent content) + { + PublicAccessChecker sut = CreateSut(memberManager, publicAccessService, contentService, out HttpContext httpContext); + httpContext.User = GetLoggedInUser(); + MockGetUserAsync(memberManager, new MemberIdentityUser { UserName = "MyUsername", IsApproved = true }); + MockGetRolesAsync(memberManager); + Mock.Get(contentService).Setup(x => x.GetById(123)).Returns(content); + Mock.Get(publicAccessService).Setup(x => x.GetEntryForContent(content)).Returns(GetPublicAccessEntry(string.Empty, "role1")); + + var result = await sut.HasMemberAccessToContentAsync(123); + Assert.AreEqual(PublicAccessStatus.AccessAccepted, result); + } } } diff --git a/src/Umbraco.Web.Common/Security/MemberManager.cs b/src/Umbraco.Web.Common/Security/MemberManager.cs index 24314a99ec43..606d4d76180e 100644 --- a/src/Umbraco.Web.Common/Security/MemberManager.cs +++ b/src/Umbraco.Web.Common/Security/MemberManager.cs @@ -30,6 +30,6 @@ public MemberManager( } public bool IsMemberAuthorized(IEnumerable allowTypes = null, IEnumerable allowGroups = null, IEnumerable allowMembers = null) - => true; // TODO: Implement! + => throw new NotImplementedException(); // TODO: Implement! } } diff --git a/src/Umbraco.Web.Common/Security/PublicAccessChecker.cs b/src/Umbraco.Web.Common/Security/PublicAccessChecker.cs index b7fdc4f1716c..8e6174e5f1d0 100644 --- a/src/Umbraco.Web.Common/Security/PublicAccessChecker.cs +++ b/src/Umbraco.Web.Common/Security/PublicAccessChecker.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -36,9 +37,9 @@ public async Task HasMemberAccessToContentAsync(int publishe } var username = currentMember.UserName; - System.Collections.Generic.IList userRoles = await memberManager.GetRolesAsync(currentMember); + IList userRoles = await memberManager.GetRolesAsync(currentMember); - if (!_publicAccessService.HasAccess(publishedContentId, _contentService, username, userRoles)) + if (userRoles.Count == 0) { return PublicAccessStatus.AccessDenied; } @@ -53,6 +54,11 @@ public async Task HasMemberAccessToContentAsync(int publishe return PublicAccessStatus.LockedOut; } + if (!_publicAccessService.HasAccess(publishedContentId, _contentService, username, userRoles)) + { + return PublicAccessStatus.AccessDenied; + } + return PublicAccessStatus.AccessAccepted; } } From 3a31c736ad9e7408a9cdf0de860b10fea0cbe28c Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 13 Apr 2021 12:11:13 +1000 Subject: [PATCH 03/26] remove PublicAccessComposer --- .../Compose/PublicAccessComposer.cs | 17 --------- .../UmbracoBuilder.Events.cs | 38 +++++++++++++------ .../DependencyInjection/UmbracoBuilder.cs | 6 +++ .../AuditNotificationsHandler.cs | 4 +- .../PublicAccessHandler.cs | 2 +- .../Compose/NotificationsComposer.cs | 1 + 6 files changed, 37 insertions(+), 31 deletions(-) delete mode 100644 src/Umbraco.Core/Compose/PublicAccessComposer.cs rename src/Umbraco.Core/{Compose => Handlers}/AuditNotificationsHandler.cs (98%) rename src/Umbraco.Core/{Compose => Handlers}/PublicAccessHandler.cs (97%) diff --git a/src/Umbraco.Core/Compose/PublicAccessComposer.cs b/src/Umbraco.Core/Compose/PublicAccessComposer.cs deleted file mode 100644 index 3f6e20c17f89..000000000000 --- a/src/Umbraco.Core/Compose/PublicAccessComposer.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Umbraco.Cms.Core.Composing; -using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Core.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 : ICoreComposer - { - public void Compose(IUmbracoBuilder builder) => - builder - .AddNotificationHandler() - .AddNotificationHandler(); - } -} diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs index 586f1d7d9948..6af917078cee 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs @@ -23,14 +23,7 @@ public static IUmbracoBuilder AddNotificationHandler where TNotification : INotification { - // Register the handler as transient. This ensures that anything can be injected into it. - var descriptor = new UniqueServiceDescriptor(typeof(INotificationHandler), typeof(TNotificationHandler), ServiceLifetime.Transient); - - if (!builder.Services.Contains(descriptor)) - { - builder.Services.Add(descriptor); - } - + builder.Services.AddNotificationHandler(); return builder; } @@ -44,16 +37,39 @@ public static IUmbracoBuilder AddNotificationHandler(this IUmbracoBuilder builder) where TNotificationAsyncHandler : INotificationAsyncHandler where TNotification : INotification + { + builder.Services.AddNotificationAsyncHandler(); + return builder; + } + + internal static IServiceCollection AddNotificationHandler(this IServiceCollection services) + where TNotificationHandler : INotificationHandler + where TNotification : INotification + { + // Register the handler as transient. This ensures that anything can be injected into it. + var descriptor = new UniqueServiceDescriptor(typeof(INotificationHandler), typeof(TNotificationHandler), ServiceLifetime.Transient); + + if (!services.Contains(descriptor)) + { + services.Add(descriptor); + } + + return services; + } + + internal static IServiceCollection AddNotificationAsyncHandler(this IServiceCollection services) + where TNotificationAsyncHandler : INotificationAsyncHandler + where TNotification : INotification { // Register the handler as transient. This ensures that anything can be injected into it. var descriptor = new ServiceDescriptor(typeof(INotificationAsyncHandler), typeof(TNotificationAsyncHandler), ServiceLifetime.Transient); - if (!builder.Services.Contains(descriptor)) + if (!services.Contains(descriptor)) { - builder.Services.Add(descriptor); + services.Add(descriptor); } - return builder; + return services; } } } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 82cf6ffa8406..add0ec3fa724 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -19,6 +19,7 @@ using Umbraco.Cms.Core.Editors; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Features; +using Umbraco.Cms.Core.Handlers; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Install; using Umbraco.Cms.Core.IO; @@ -31,6 +32,7 @@ using Umbraco.Cms.Core.Runtime; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Notifications; using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Core.Templates; using Umbraco.Cms.Core.Web; @@ -219,6 +221,10 @@ private void AddCoreServices() // which may be replaced by models builder but the default is required to make plain old IPublishedContent // instances. Services.AddSingleton(factory => factory.CreateDefaultPublishedModelFactory()); + + Services + .AddNotificationHandler() + .AddNotificationHandler(); } } } diff --git a/src/Umbraco.Core/Compose/AuditNotificationsHandler.cs b/src/Umbraco.Core/Handlers/AuditNotificationsHandler.cs similarity index 98% rename from src/Umbraco.Core/Compose/AuditNotificationsHandler.cs rename to src/Umbraco.Core/Handlers/AuditNotificationsHandler.cs index 1347da05fda4..06a7743fee9d 100644 --- a/src/Umbraco.Core/Compose/AuditNotificationsHandler.cs +++ b/src/Umbraco.Core/Handlers/AuditNotificationsHandler.cs @@ -12,7 +12,7 @@ using Umbraco.Cms.Core.Services.Notifications; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Compose +namespace Umbraco.Cms.Core.Handlers { public sealed class AuditNotificationsHandler : INotificationHandler, @@ -61,7 +61,7 @@ private IUser CurrentPerformingUser } } - public static IUser UnknownUser(GlobalSettings globalSettings) => new User(globalSettings) { Id = Cms.Core.Constants.Security.UnknownUserId, Name = Cms.Core.Constants.Security.UnknownUserName, Email = "" }; + public static IUser UnknownUser(GlobalSettings globalSettings) => new User(globalSettings) { Id = Constants.Security.UnknownUserId, Name = Constants.Security.UnknownUserName, Email = "" }; private string PerformingIp => _ipResolver.GetCurrentRequestIpAddress(); diff --git a/src/Umbraco.Core/Compose/PublicAccessHandler.cs b/src/Umbraco.Core/Handlers/PublicAccessHandler.cs similarity index 97% rename from src/Umbraco.Core/Compose/PublicAccessHandler.cs rename to src/Umbraco.Core/Handlers/PublicAccessHandler.cs index a677db25d440..9645e9a88c67 100644 --- a/src/Umbraco.Core/Compose/PublicAccessHandler.cs +++ b/src/Umbraco.Core/Handlers/PublicAccessHandler.cs @@ -6,7 +6,7 @@ using Umbraco.Cms.Core.Services.Notifications; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Compose +namespace Umbraco.Cms.Core.Handlers { public sealed class PublicAccessHandler : INotificationHandler, diff --git a/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs b/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs index 3fb7bd34ac60..3477ccf381e0 100644 --- a/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs +++ b/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs @@ -5,6 +5,7 @@ using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Handlers; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services.Notifications; From 41a10dc4b82bc0af8136413eee4d81bce5170429 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 14 Apr 2021 14:55:26 +1000 Subject: [PATCH 04/26] adjust namespaces, ensure RoleManager works, separate public access controller, reduce content controller --- src/Umbraco.Core/Cache/CacheRefresherBase.cs | 7 +- .../Repositories/IExternalLoginRepository.cs | 2 +- .../Identity => Security}/ExternalLogin.cs | 4 +- .../ExternalLoginToken.cs | 4 +- .../Identity => Security}/IExternalLogin.cs | 2 +- .../IExternalLoginToken.cs | 2 +- .../IIdentityUserLogin.cs | 2 +- .../IIdentityUserToken.cs | 2 +- .../IdentityUserLogin.cs | 2 +- .../IdentityUserToken.cs | 2 +- .../Services/IExternalLoginService.cs | 2 +- .../Factories/ExternalLoginFactory.cs | 2 +- .../Mappers/ExternalLoginMapper.cs | 2 +- .../Mappers/ExternalLoginTokenMapper.cs | 2 +- .../Implement/ExternalLoginRepository.cs | 2 +- .../Security/BackOfficeIdentityUser.cs | 1 - .../Security/BackOfficeUserStore.cs | 2 - .../Security/IUmbracoUserManager.cs | 1 - .../Security/MemberIdentityUser.cs | 1 - .../Security/MemberRoleStore.cs | 8 +- .../Security/MemberUserStore.cs | 1 - .../Security/UmbracoIdentityRole.cs | 2 +- .../Security/UmbracoIdentityUser.cs | 2 +- .../Security/UmbracoUserManager.cs | 1 - .../Implement/ExternalLoginService.cs | 2 +- .../Builders/UmbracoIdentityRoleBuilder.cs | 2 +- .../Builders/UserBuilder.cs | 1 - .../Services/ExternalLoginServiceTests.cs | 2 +- .../Security/MemberRoleStoreTests.cs | 1 - .../Controllers/BackOfficeServerVariables.cs | 4 + .../Controllers/ContentController.cs | 156 +----- .../Controllers/PublicAccessController.cs | 187 ++++++++ .../UmbracoBuilderExtensions.cs | 2 - .../Mapping/MemberMapDefinition.cs | 14 +- .../Security/IPasswordChanger.cs | 1 - .../Security/PasswordChanger.cs | 1 - .../ServiceCollectionExtensions.cs | 11 +- .../Extensions/IdentityBuilderExtensions.cs | 9 + .../Security/IMemberRoleManager.cs | 10 + .../Security/MemberManager.cs | 1 - .../Security/MemberRoleManager.cs | 20 + .../Security/UmbracoSignInManager.cs | 2 +- .../src/common/resources/content.resource.js | 109 ----- .../common/resources/publicaccess.resource.js | 128 +++++ .../content/content.protect.controller.js | 450 +++++++++--------- 45 files changed, 641 insertions(+), 530 deletions(-) rename src/Umbraco.Core/{Models/Identity => Security}/ExternalLogin.cs (89%) rename src/Umbraco.Core/{Models/Identity => Security}/ExternalLoginToken.cs (92%) rename src/Umbraco.Core/{Models/Identity => Security}/IExternalLogin.cs (91%) rename src/Umbraco.Core/{Models/Identity => Security}/IExternalLoginToken.cs (91%) rename src/Umbraco.Core/{Models/Identity => Security}/IIdentityUserLogin.cs (95%) rename src/Umbraco.Core/{Models/Identity => Security}/IIdentityUserToken.cs (94%) rename src/Umbraco.Core/{Models/Identity => Security}/IdentityUserLogin.cs (96%) rename src/Umbraco.Core/{Models/Identity => Security}/IdentityUserToken.cs (97%) create mode 100644 src/Umbraco.Web.BackOffice/Controllers/PublicAccessController.cs create mode 100644 src/Umbraco.Web.Common/Security/IMemberRoleManager.cs create mode 100644 src/Umbraco.Web.Common/Security/MemberRoleManager.cs create mode 100644 src/Umbraco.Web.UI.Client/src/common/resources/publicaccess.resource.js diff --git a/src/Umbraco.Core/Cache/CacheRefresherBase.cs b/src/Umbraco.Core/Cache/CacheRefresherBase.cs index c5f3d903abd5..323fa6aecaa0 100644 --- a/src/Umbraco.Core/Cache/CacheRefresherBase.cs +++ b/src/Umbraco.Core/Cache/CacheRefresherBase.cs @@ -50,7 +50,12 @@ protected CacheRefresherBase(AppCaches appCaches, IEventAggregator eventAggregat /// public virtual void RefreshAll() { - OnCacheUpdated(NotificationFactory.Create(null, MessageType.RefreshAll)); + // NOTE: We pass in string.Empty here because if we pass in NULL this causes problems with + // the underlying ActivatorUtilities.CreateInstance which doesn't seem to support passing in + // null to an 'object' parameter and we end up with "A suitable constructor for type 'ZYZ' could not be located." + // In this case, all cache refreshers should be checking for the type first before checking for a msg value + // so this shouldn't cause any issues. + OnCacheUpdated(NotificationFactory.Create(string.Empty, MessageType.RefreshAll)); } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs index 9f755c225632..a685ab67f14d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Umbraco.Cms.Core.Models.Identity; +using Umbraco.Cms.Core.Security; namespace Umbraco.Cms.Core.Persistence.Repositories { diff --git a/src/Umbraco.Core/Models/Identity/ExternalLogin.cs b/src/Umbraco.Core/Security/ExternalLogin.cs similarity index 89% rename from src/Umbraco.Core/Models/Identity/ExternalLogin.cs rename to src/Umbraco.Core/Security/ExternalLogin.cs index c599dcab96ef..48bbfa2091b3 100644 --- a/src/Umbraco.Core/Models/Identity/ExternalLogin.cs +++ b/src/Umbraco.Core/Security/ExternalLogin.cs @@ -1,6 +1,6 @@ using System; -namespace Umbraco.Cms.Core.Models.Identity +namespace Umbraco.Cms.Core.Security { /// @@ -20,7 +20,7 @@ public ExternalLogin(string loginProvider, string providerKey, string userData = public string LoginProvider { get; } /// - public string ProviderKey { get; } + public string ProviderKey { get; } /// public string UserData { get; } diff --git a/src/Umbraco.Core/Models/Identity/ExternalLoginToken.cs b/src/Umbraco.Core/Security/ExternalLoginToken.cs similarity index 92% rename from src/Umbraco.Core/Models/Identity/ExternalLoginToken.cs rename to src/Umbraco.Core/Security/ExternalLoginToken.cs index 23f532fdfa31..85089ddba625 100644 --- a/src/Umbraco.Core/Models/Identity/ExternalLoginToken.cs +++ b/src/Umbraco.Core/Security/ExternalLoginToken.cs @@ -1,6 +1,6 @@ -using System; +using System; -namespace Umbraco.Cms.Core.Models.Identity +namespace Umbraco.Cms.Core.Security { /// public class ExternalLoginToken : IExternalLoginToken diff --git a/src/Umbraco.Core/Models/Identity/IExternalLogin.cs b/src/Umbraco.Core/Security/IExternalLogin.cs similarity index 91% rename from src/Umbraco.Core/Models/Identity/IExternalLogin.cs rename to src/Umbraco.Core/Security/IExternalLogin.cs index 1cc570c36f88..b285bd35eb9d 100644 --- a/src/Umbraco.Core/Models/Identity/IExternalLogin.cs +++ b/src/Umbraco.Core/Security/IExternalLogin.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Core.Models.Identity +namespace Umbraco.Cms.Core.Security { /// /// Used to persist external login data for a user diff --git a/src/Umbraco.Core/Models/Identity/IExternalLoginToken.cs b/src/Umbraco.Core/Security/IExternalLoginToken.cs similarity index 91% rename from src/Umbraco.Core/Models/Identity/IExternalLoginToken.cs rename to src/Umbraco.Core/Security/IExternalLoginToken.cs index 7cc3065e35d2..b3fd4b64b2e8 100644 --- a/src/Umbraco.Core/Models/Identity/IExternalLoginToken.cs +++ b/src/Umbraco.Core/Security/IExternalLoginToken.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Core.Models.Identity +namespace Umbraco.Cms.Core.Security { /// /// Used to persist an external login token for a user diff --git a/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs b/src/Umbraco.Core/Security/IIdentityUserLogin.cs similarity index 95% rename from src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs rename to src/Umbraco.Core/Security/IIdentityUserLogin.cs index 21d8f842af36..67ca739509a2 100644 --- a/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs +++ b/src/Umbraco.Core/Security/IIdentityUserLogin.cs @@ -1,6 +1,6 @@ using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Identity +namespace Umbraco.Cms.Core.Security { /// /// An external login provider linked to a user diff --git a/src/Umbraco.Core/Models/Identity/IIdentityUserToken.cs b/src/Umbraco.Core/Security/IIdentityUserToken.cs similarity index 94% rename from src/Umbraco.Core/Models/Identity/IIdentityUserToken.cs rename to src/Umbraco.Core/Security/IIdentityUserToken.cs index c3a451f31de1..d7d3af6adfa0 100644 --- a/src/Umbraco.Core/Models/Identity/IIdentityUserToken.cs +++ b/src/Umbraco.Core/Security/IIdentityUserToken.cs @@ -1,6 +1,6 @@ using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Identity +namespace Umbraco.Cms.Core.Security { /// /// An external login provider token diff --git a/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs b/src/Umbraco.Core/Security/IdentityUserLogin.cs similarity index 96% rename from src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs rename to src/Umbraco.Core/Security/IdentityUserLogin.cs index b719d9cd513a..4775b9d3e603 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs +++ b/src/Umbraco.Core/Security/IdentityUserLogin.cs @@ -1,7 +1,7 @@ using System; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Identity +namespace Umbraco.Cms.Core.Security { /// diff --git a/src/Umbraco.Core/Models/Identity/IdentityUserToken.cs b/src/Umbraco.Core/Security/IdentityUserToken.cs similarity index 97% rename from src/Umbraco.Core/Models/Identity/IdentityUserToken.cs rename to src/Umbraco.Core/Security/IdentityUserToken.cs index 42e79a89dd51..4a3c0f21cf5e 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityUserToken.cs +++ b/src/Umbraco.Core/Security/IdentityUserToken.cs @@ -1,7 +1,7 @@ using System; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Identity +namespace Umbraco.Cms.Core.Security { public class IdentityUserToken : EntityBase, IIdentityUserToken { diff --git a/src/Umbraco.Core/Services/IExternalLoginService.cs b/src/Umbraco.Core/Services/IExternalLoginService.cs index 8834a4b33f37..787631d50000 100644 --- a/src/Umbraco.Core/Services/IExternalLoginService.cs +++ b/src/Umbraco.Core/Services/IExternalLoginService.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Umbraco.Cms.Core.Models.Identity; +using Umbraco.Cms.Core.Security; namespace Umbraco.Cms.Core.Services { diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs index 772545e2a35b..58819f306a90 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs @@ -1,5 +1,5 @@ using System; -using Umbraco.Cms.Core.Models.Identity; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Infrastructure.Persistence.Dtos; namespace Umbraco.Cms.Infrastructure.Persistence.Factories diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginMapper.cs index 198554be0cf0..2d47746baa9b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginMapper.cs @@ -1,5 +1,5 @@ using System; -using Umbraco.Cms.Core.Models.Identity; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Infrastructure.Persistence.Dtos; namespace Umbraco.Cms.Infrastructure.Persistence.Mappers diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginTokenMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginTokenMapper.cs index 11cb60dc833c..4d03031ffdeb 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginTokenMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginTokenMapper.cs @@ -1,5 +1,5 @@ using System; -using Umbraco.Cms.Core.Models.Identity; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Infrastructure.Persistence.Dtos; namespace Umbraco.Cms.Infrastructure.Persistence.Mappers diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs index a262636bfcd0..2eec8b661b12 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs @@ -5,11 +5,11 @@ using NPoco; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models.Entities; -using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Persistence.Querying; diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs index 843349b9fdac..68aae8340abd 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs @@ -3,7 +3,6 @@ using System.Linq; using Microsoft.AspNetCore.Identity; using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Extensions; diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs index 99d9d35cea0d..489fab835d62 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs @@ -8,12 +8,10 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; diff --git a/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs b/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs index 90f7f766f9fc..6acf52c3cb95 100644 --- a/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs @@ -3,7 +3,6 @@ using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; -using Umbraco.Cms.Core.Models.Identity; namespace Umbraco.Cms.Core.Security { diff --git a/src/Umbraco.Infrastructure/Security/MemberIdentityUser.cs b/src/Umbraco.Infrastructure/Security/MemberIdentityUser.cs index b9165e18afea..22b4e2645bdb 100644 --- a/src/Umbraco.Infrastructure/Security/MemberIdentityUser.cs +++ b/src/Umbraco.Infrastructure/Security/MemberIdentityUser.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Identity; -using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Extensions; diff --git a/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs b/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs index da4db4b60644..a87b3c7f7ed2 100644 --- a/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs @@ -1,9 +1,10 @@ using System; +using System.Collections.Generic; +using System.Linq; 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 @@ -11,7 +12,7 @@ namespace Umbraco.Cms.Core.Security /// /// A custom user store that uses Umbraco member data /// - public class MemberRoleStore : IRoleStore + public class MemberRoleStore : IRoleStore, IQueryableRoleStore { private readonly IMemberGroupService _memberGroupService; private bool _disposed; @@ -33,6 +34,8 @@ public MemberRoleStore(IMemberGroupService memberGroupService, IdentityErrorDesc /// public IdentityErrorDescriber ErrorDescriber { get; set; } + public IQueryable Roles => _memberGroupService.GetAll().Select(MapFromMemberGroup).AsQueryable(); + /// public Task CreateAsync(UmbracoIdentityRole role, CancellationToken cancellationToken = default) { @@ -268,5 +271,6 @@ protected void ThrowIfDisposed() throw new ObjectDisposedException(GetType().Name); } } + } } diff --git a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs index 53420ff66713..c752e41bb221 100644 --- a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs @@ -9,7 +9,6 @@ using Microsoft.AspNetCore.Identity; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; diff --git a/src/Umbraco.Infrastructure/Security/UmbracoIdentityRole.cs b/src/Umbraco.Infrastructure/Security/UmbracoIdentityRole.cs index 00c4038287a4..ccf9448604eb 100644 --- a/src/Umbraco.Infrastructure/Security/UmbracoIdentityRole.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoIdentityRole.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Identity; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Identity +namespace Umbraco.Cms.Core.Security { public class UmbracoIdentityRole : IdentityRole, IRememberBeingDirty { diff --git a/src/Umbraco.Infrastructure/Security/UmbracoIdentityUser.cs b/src/Umbraco.Infrastructure/Security/UmbracoIdentityUser.cs index bf553b3d3008..978f7650d320 100644 --- a/src/Umbraco.Infrastructure/Security/UmbracoIdentityUser.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoIdentityUser.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Identity; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Identity +namespace Umbraco.Cms.Core.Security { /// diff --git a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs index 1af789628139..7f77b9d8c61f 100644 --- a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs @@ -7,7 +7,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration; -using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Net; namespace Umbraco.Cms.Core.Security diff --git a/src/Umbraco.Infrastructure/Services/Implement/ExternalLoginService.cs b/src/Umbraco.Infrastructure/Services/Implement/ExternalLoginService.cs index 9e4e438150c7..259d6e8739bc 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ExternalLoginService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/ExternalLoginService.cs @@ -2,9 +2,9 @@ using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Security; namespace Umbraco.Cms.Core.Services.Implement { diff --git a/src/Umbraco.Tests.Common/Builders/UmbracoIdentityRoleBuilder.cs b/src/Umbraco.Tests.Common/Builders/UmbracoIdentityRoleBuilder.cs index 6ffe4fd5c579..f2dfad6e22cf 100644 --- a/src/Umbraco.Tests.Common/Builders/UmbracoIdentityRoleBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/UmbracoIdentityRoleBuilder.cs @@ -1,7 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using Umbraco.Cms.Core.Models.Identity; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Tests.Common.Builders.Interfaces; namespace Umbraco.Cms.Tests.Common.Builders diff --git a/src/Umbraco.Tests.Common/Builders/UserBuilder.cs b/src/Umbraco.Tests.Common/Builders/UserBuilder.cs index 9d00962a9fa0..f1149bca5641 100644 --- a/src/Umbraco.Tests.Common/Builders/UserBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/UserBuilder.cs @@ -5,7 +5,6 @@ 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; diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ExternalLoginServiceTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ExternalLoginServiceTests.cs index e56891601c46..fc183e0ce480 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ExternalLoginServiceTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ExternalLoginServiceTests.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Threading; using NUnit.Framework; -using Umbraco.Cms.Core.Models.Identity; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Tests.Common.Builders; diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs index 15f4b7f30df6..412de11a9e5b 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs @@ -6,7 +6,6 @@ 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; diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs index 9070c55439ee..3aa3239dbc5a 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs @@ -188,6 +188,10 @@ internal Task> GetServerVariablesAsync() "contentApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( controller => controller.PostSave(null)) }, + { + "publicAccessApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( + controller => controller.GetPublicAccess(0)) + }, { "mediaApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( controller => controller.GetRootMedia()) diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index 9518a9b84dee..2c8d4bc385d1 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -53,19 +53,15 @@ public class ContentController : ContentControllerBase private readonly ILocalizedTextService _localizedTextService; private readonly IUserService _userService; private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; - private readonly IEntityService _entityService; private readonly IContentTypeService _contentTypeService; private readonly UmbracoMapper _umbracoMapper; - private readonly IPublishedUrlProvider _publishedUrlProvider; - private readonly IPublicAccessService _publicAccessService; + private readonly IPublishedUrlProvider _publishedUrlProvider; private readonly IDomainService _domainService; private readonly IDataTypeService _dataTypeService; - private readonly ILocalizationService _localizationService; - private readonly IMemberService _memberService; + private readonly ILocalizationService _localizationService; private readonly IFileService _fileService; private readonly INotificationService _notificationService; - private readonly ActionCollection _actionCollection; - private readonly IMemberGroupService _memberGroupService; + private readonly ActionCollection _actionCollection; private readonly ISqlContext _sqlContext; private readonly IAuthorizationService _authorizationService; private readonly Lazy> _allLangs; @@ -83,19 +79,15 @@ public ContentController( IContentService contentService, IUserService userService, IBackOfficeSecurityAccessor backofficeSecurityAccessor, - IEntityService entityService, IContentTypeService contentTypeService, UmbracoMapper umbracoMapper, IPublishedUrlProvider publishedUrlProvider, - IPublicAccessService publicAccessService, IDomainService domainService, IDataTypeService dataTypeService, ILocalizationService localizationService, - IMemberService memberService, IFileService fileService, INotificationService notificationService, ActionCollection actionCollection, - IMemberGroupService memberGroupService, ISqlContext sqlContext, IJsonSerializer serializer, IAuthorizationService authorizationService) @@ -106,19 +98,15 @@ public ContentController( _localizedTextService = localizedTextService; _userService = userService; _backofficeSecurityAccessor = backofficeSecurityAccessor; - _entityService = entityService; _contentTypeService = contentTypeService; _umbracoMapper = umbracoMapper; _publishedUrlProvider = publishedUrlProvider; - _publicAccessService = publicAccessService; _domainService = domainService; _dataTypeService = dataTypeService; _localizationService = localizationService; - _memberService = memberService; _fileService = fileService; _notificationService = notificationService; _actionCollection = actionCollection; - _memberGroupService = memberGroupService; _sqlContext = sqlContext; _authorizationService = authorizationService; _logger = loggerFactory.CreateLogger(); @@ -2403,142 +2391,8 @@ public IActionResult PostRollbackContent(int contentId, int versionId, string cu return new ValidationErrorResult(notificationModel); } - [Authorize(Policy = AuthorizationPolicies.ContentPermissionProtectById)] - [HttpGet] - public IActionResult GetPublicAccess(int contentId) - { - var content = _contentService.GetById(contentId); - if (content == null) - { - return NotFound(); - } - - var entry = _publicAccessService.GetEntryForContent(content); - if (entry == null || entry.ProtectedNodeId != content.Id) - { - return Ok(); - } - - var loginPageEntity = _entityService.Get(entry.LoginNodeId, UmbracoObjectTypes.Document); - var errorPageEntity = _entityService.Get(entry.NoAccessNodeId, UmbracoObjectTypes.Document); - - // unwrap the current public access setup for the client - // - this API method is the single point of entry for both "modes" of public access (single user and role based) - var usernames = entry.Rules - .Where(rule => rule.RuleType == Constants.Conventions.PublicAccess.MemberUsernameRuleType) - .Select(rule => rule.RuleValue).ToArray(); - - var members = usernames - .Select(username => _memberService.GetByUsername(username)) - .Where(member => member != null) - .Select(_umbracoMapper.Map) - .ToArray(); - - //TODO: change to role manager - var allGroups = _memberGroupService.GetAll().ToArray(); - var groups = entry.Rules - .Where(rule => rule.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType) - .Select(rule => allGroups.FirstOrDefault(g => g.Name == rule.RuleValue)) - .Where(memberGroup => memberGroup != null) - .Select(_umbracoMapper.Map) - .ToArray(); - - return Ok(new PublicAccess - { - Members = members, - Groups = groups, - LoginPage = loginPageEntity != null ? _umbracoMapper.Map(loginPageEntity) : null, - ErrorPage = errorPageEntity != null ? _umbracoMapper.Map(errorPageEntity) : null - }); - } - - // set up public access using role based access - [Authorize(Policy = AuthorizationPolicies.ContentPermissionProtectById)] - [HttpPost] - public IActionResult PostPublicAccess(int contentId, [FromQuery(Name = "groups[]")] string[] groups, [FromQuery(Name = "usernames[]")] string[] usernames, int loginPageId, int errorPageId) - { - if ((groups == null || groups.Any() == false) && (usernames == null || usernames.Any() == false)) - { - return BadRequest(); - } - - var content = _contentService.GetById(contentId); - var loginPage = _contentService.GetById(loginPageId); - var errorPage = _contentService.GetById(errorPageId); - if (content == null || loginPage == null || errorPage == null) - { - return BadRequest(); - } - - var isGroupBased = groups != null && groups.Any(); - var candidateRuleValues = isGroupBased - ? groups - : usernames; - var newRuleType = isGroupBased - ? Constants.Conventions.PublicAccess.MemberRoleRuleType - : Constants.Conventions.PublicAccess.MemberUsernameRuleType; - - var entry = _publicAccessService.GetEntryForContent(content); - - if (entry == null || entry.ProtectedNodeId != content.Id) - { - entry = new PublicAccessEntry(content, loginPage, errorPage, new List()); + - foreach (var ruleValue in candidateRuleValues) - { - entry.AddRule(ruleValue, newRuleType); - } - } - else - { - entry.LoginNodeId = loginPage.Id; - entry.NoAccessNodeId = errorPage.Id; - - var currentRules = entry.Rules.ToArray(); - var obsoleteRules = currentRules.Where(rule => - rule.RuleType != newRuleType - || candidateRuleValues.Contains(rule.RuleValue) == false - ); - var newRuleValues = candidateRuleValues.Where(group => - currentRules.Any(rule => - rule.RuleType == newRuleType - && rule.RuleValue == group - ) == false - ); - foreach (var rule in obsoleteRules) - { - entry.RemoveRule(rule); - } - foreach (var ruleValue in newRuleValues) - { - entry.AddRule(ruleValue, newRuleType); - } - } - - return _publicAccessService.Save(entry).Success - ? (IActionResult)Ok() - : Problem(); - } - - [Authorize(Policy = AuthorizationPolicies.ContentPermissionProtectById)] - [HttpPost] - public IActionResult RemovePublicAccess(int contentId) - { - var content = _contentService.GetById(contentId); - if (content == null) - { - return NotFound(); - } - - var entry = _publicAccessService.GetEntryForContent(content); - if (entry == null) - { - return Ok(); - } - - return _publicAccessService.Delete(entry).Success - ? (IActionResult)Ok() - : Problem(); - } + } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/PublicAccessController.cs b/src/Umbraco.Web.BackOffice/Controllers/PublicAccessController.cs new file mode 100644 index 000000000000..3b347c3d7636 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Controllers/PublicAccessController.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Attributes; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Cms.Web.Common.Security; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Web.BackOffice.Controllers +{ + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessDocuments)] + public class PublicAccessController : BackOfficeNotificationsController + { + private readonly IContentService _contentService; + private readonly IPublicAccessService _publicAccessService; + private readonly IEntityService _entityService; + private readonly IMemberService _memberService; + private readonly UmbracoMapper _umbracoMapper; + private readonly IMemberRoleManager _memberRoleManager; + + public PublicAccessController( + IPublicAccessService publicAccessService, + IContentService contentService, + IEntityService entityService, + IMemberService memberService, + UmbracoMapper umbracoMapper, + IMemberRoleManager memberRoleManager) + { + _contentService = contentService; + _publicAccessService = publicAccessService; + _entityService = entityService; + _memberService = memberService; + _umbracoMapper = umbracoMapper; + _memberRoleManager = memberRoleManager; + } + + [Authorize(Policy = AuthorizationPolicies.ContentPermissionProtectById)] + [HttpGet] + public ActionResult GetPublicAccess(int contentId) + { + IContent content = _contentService.GetById(contentId); + if (content == null) + { + return NotFound(); + } + + PublicAccessEntry entry = _publicAccessService.GetEntryForContent(content); + if (entry == null || entry.ProtectedNodeId != content.Id) + { + return Ok(); + } + + IEntitySlim loginPageEntity = _entityService.Get(entry.LoginNodeId, UmbracoObjectTypes.Document); + IEntitySlim errorPageEntity = _entityService.Get(entry.NoAccessNodeId, UmbracoObjectTypes.Document); + + // unwrap the current public access setup for the client + // - this API method is the single point of entry for both "modes" of public access (single user and role based) + var usernames = entry.Rules + .Where(rule => rule.RuleType == Constants.Conventions.PublicAccess.MemberUsernameRuleType) + .Select(rule => rule.RuleValue) + .ToArray(); + + MemberDisplay[] members = usernames + .Select(username => _memberService.GetByUsername(username)) + .Where(member => member != null) + .Select(_umbracoMapper.Map) + .ToArray(); + + var allGroups = _memberRoleManager.Roles.ToDictionary(x => x.Name); + MemberGroupDisplay[] groups = entry.Rules + .Where(rule => rule.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType) + .Select(rule => allGroups.TryGetValue(rule.RuleValue, out UmbracoIdentityRole memberRole) ? memberRole : null) + .Where(x => x != null) + .Select(_umbracoMapper.Map) + .ToArray(); + + return new PublicAccess + { + Members = members, + Groups = groups, + LoginPage = loginPageEntity != null ? _umbracoMapper.Map(loginPageEntity) : null, + ErrorPage = errorPageEntity != null ? _umbracoMapper.Map(errorPageEntity) : null + }; + } + + // set up public access using role based access + [Authorize(Policy = AuthorizationPolicies.ContentPermissionProtectById)] + [HttpPost] + public IActionResult PostPublicAccess(int contentId, [FromQuery(Name = "groups[]")] string[] groups, [FromQuery(Name = "usernames[]")] string[] usernames, int loginPageId, int errorPageId) + { + if ((groups == null || groups.Any() == false) && (usernames == null || usernames.Any() == false)) + { + return BadRequest(); + } + + var content = _contentService.GetById(contentId); + var loginPage = _contentService.GetById(loginPageId); + var errorPage = _contentService.GetById(errorPageId); + if (content == null || loginPage == null || errorPage == null) + { + return BadRequest(); + } + + var isGroupBased = groups != null && groups.Any(); + var candidateRuleValues = isGroupBased + ? groups + : usernames; + var newRuleType = isGroupBased + ? Constants.Conventions.PublicAccess.MemberRoleRuleType + : Constants.Conventions.PublicAccess.MemberUsernameRuleType; + + var entry = _publicAccessService.GetEntryForContent(content); + + if (entry == null || entry.ProtectedNodeId != content.Id) + { + entry = new PublicAccessEntry(content, loginPage, errorPage, new List()); + + foreach (var ruleValue in candidateRuleValues) + { + entry.AddRule(ruleValue, newRuleType); + } + } + else + { + entry.LoginNodeId = loginPage.Id; + entry.NoAccessNodeId = errorPage.Id; + + var currentRules = entry.Rules.ToArray(); + var obsoleteRules = currentRules.Where(rule => + rule.RuleType != newRuleType + || candidateRuleValues.Contains(rule.RuleValue) == false + ); + var newRuleValues = candidateRuleValues.Where(group => + currentRules.Any(rule => + rule.RuleType == newRuleType + && rule.RuleValue == group + ) == false + ); + foreach (var rule in obsoleteRules) + { + entry.RemoveRule(rule); + } + foreach (var ruleValue in newRuleValues) + { + entry.AddRule(ruleValue, newRuleType); + } + } + + return _publicAccessService.Save(entry).Success + ? Ok() + : Problem(); + } + + [Authorize(Policy = AuthorizationPolicies.ContentPermissionProtectById)] + [HttpPost] + public IActionResult RemovePublicAccess(int contentId) + { + var content = _contentService.GetById(contentId); + if (content == null) + { + return NotFound(); + } + + var entry = _publicAccessService.GetEntryForContent(content); + if (entry == null) + { + return Ok(); + } + + return _publicAccessService.Delete(entry).Success + ? Ok() + : Problem(); + } + } +} diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index 422b8a87ff68..7ce9a83a3e10 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -9,7 +9,6 @@ using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; -using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.DependencyInjection; @@ -25,7 +24,6 @@ using Umbraco.Cms.Web.BackOffice.Trees; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Cms.Web.Common.Security; -using IWebHostEnvironment = Microsoft.AspNetCore.Hosting.IWebHostEnvironment; namespace Umbraco.Extensions { diff --git a/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs index b5592b08ff6f..4b2837d9b809 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs @@ -6,6 +6,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Mapping; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Web.BackOffice.Trees; using Constants = Umbraco.Cms.Core.Constants; @@ -32,6 +33,7 @@ public void DefineMaps(UmbracoMapper mapper) 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); } @@ -93,7 +95,17 @@ private void Map(IMemberGroup source, MemberGroupDisplay target, MapperContext c target.Path = $"-1,{source.Id}"; target.Udi = Udi.Create(Constants.UdiEntityType.MemberGroup, source.Key); } - + + // Umbraco.Code.MapAll -Icon -Trashed -ParentId -Alias -Key -Udi + private void Map(UmbracoIdentityRole source, MemberGroupDisplay target, MapperContext context) + { + target.Id = source.Id; + //target.Key = source.Key; + target.Name = source.Name; + target.Path = $"-1,{source.Id}"; + //target.Udi = Udi.Create(Constants.UdiEntityType.MemberGroup, source.Key); + } + // Umbraco.Code.MapAll private static void Map(IMember source, ContentPropertyCollectionDto target, MapperContext context) { diff --git a/src/Umbraco.Web.BackOffice/Security/IPasswordChanger.cs b/src/Umbraco.Web.BackOffice/Security/IPasswordChanger.cs index d1f90d7bcf2e..95be44f0669a 100644 --- a/src/Umbraco.Web.BackOffice/Security/IPasswordChanger.cs +++ b/src/Umbraco.Web.BackOffice/Security/IPasswordChanger.cs @@ -1,7 +1,6 @@ using System.Threading.Tasks; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Security; namespace Umbraco.Cms.Web.Common.Security diff --git a/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs b/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs index 83f68c875416..18e3a4db48c8 100644 --- a/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs +++ b/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs @@ -5,7 +5,6 @@ using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Security; using Umbraco.Extensions; diff --git a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs index b5530f93770a..ad82e1abf527 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs @@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Web.Caching; using SixLabors.ImageSharp.Web.Commands; @@ -11,7 +10,6 @@ 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; @@ -66,12 +64,15 @@ public static IServiceCollection AddUmbracoImageSharp(this IServiceCollection se public static void AddMembersIdentity(this IServiceCollection services) { + // TODO: We may need to use services.AddIdentityCore instead if this is doing too much + services.AddIdentity() .AddDefaultTokenProviders() - .AddMemberManager() - .AddSignInManager() .AddUserStore() - .AddRoleStore(); + .AddRoleStore() + .AddRoleManager() + .AddMemberManager() + .AddSignInManager(); services.ConfigureApplicationCookie(x => { diff --git a/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs index e4c3033e48a9..14cd644a660d 100644 --- a/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs @@ -24,6 +24,15 @@ public static IdentityBuilder AddMemberManager(this Id return identityBuilder; } + public static IdentityBuilder AddRoleManager(this IdentityBuilder identityBuilder) + where TRoleManager : RoleManager, TInterface + { + identityBuilder.AddRoleManager(); + identityBuilder.Services.AddScoped(typeof(TInterface), typeof(TRoleManager)); + identityBuilder.Services.AddScoped(typeof(RoleManager), factory => factory.GetRequiredService()); + return identityBuilder; + } + /// /// Adds a implementation for /// diff --git a/src/Umbraco.Web.Common/Security/IMemberRoleManager.cs b/src/Umbraco.Web.Common/Security/IMemberRoleManager.cs new file mode 100644 index 000000000000..e10bc118be74 --- /dev/null +++ b/src/Umbraco.Web.Common/Security/IMemberRoleManager.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Security; + +namespace Umbraco.Cms.Web.Common.Security +{ + public interface IMemberRoleManager + { + IEnumerable Roles { get; } + } +} diff --git a/src/Umbraco.Web.Common/Security/MemberManager.cs b/src/Umbraco.Web.Common/Security/MemberManager.cs index 606d4d76180e..b1967c74cf66 100644 --- a/src/Umbraco.Web.Common/Security/MemberManager.cs +++ b/src/Umbraco.Web.Common/Security/MemberManager.cs @@ -7,7 +7,6 @@ using Umbraco.Cms.Core.Net; using Umbraco.Cms.Core.Security; - namespace Umbraco.Cms.Web.Common.Security { diff --git a/src/Umbraco.Web.Common/Security/MemberRoleManager.cs b/src/Umbraco.Web.Common/Security/MemberRoleManager.cs new file mode 100644 index 000000000000..c0cd18aeda98 --- /dev/null +++ b/src/Umbraco.Web.Common/Security/MemberRoleManager.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Security; + +namespace Umbraco.Cms.Web.Common.Security +{ + public class MemberRoleManager : RoleManager, IMemberRoleManager + { + public MemberRoleManager( + IRoleStore store, + IEnumerable> roleValidators, + IdentityErrorDescriber errors, + ILogger logger) + : base(store, roleValidators, new NoopLookupNormalizer(), errors, logger) { } + + IEnumerable IMemberRoleManager.Roles => base.Roles.ToList(); + } +} diff --git a/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs b/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs index ea29098befe2..061787c6f68f 100644 --- a/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs +++ b/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs @@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Umbraco.Cms.Core.Models.Identity; +using Umbraco.Cms.Core.Security; using Umbraco.Extensions; namespace Umbraco.Cms.Web.Common.Security diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index 368eab2339e3..0f359f068998 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -1197,115 +1197,6 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { ), "Failed to roll back content item with id " + contentId ); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#getPublicAccess - * @methodOf umbraco.resources.contentResource - * - * @description - * Returns the public access protection for a content item - * - * ##usage - *
-          * contentResource.getPublicAccess(contentId)
-          *    .then(function(publicAccess) {
-          *        // do your thing
-          *    });
-          * 
- * - * @param {Int} contentId The content Id - * @returns {Promise} resourcePromise object containing the public access protection - * - */ - getPublicAccess: function (contentId) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl("contentApiBaseUrl", "GetPublicAccess", { - contentId: contentId - }) - ), - "Failed to get public access for content item with id " + contentId - ); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#updatePublicAccess - * @methodOf umbraco.resources.contentResource - * - * @description - * Sets or updates the public access protection for a content item - * - * ##usage - *
-          * contentResource.updatePublicAccess(contentId, userName, password, roles, loginPageId, errorPageId)
-          *    .then(function() {
-          *        // do your thing
-          *    });
-          * 
- * - * @param {Int} contentId The content Id - * @param {Array} groups The names of the groups that should have access (if using group based protection) - * @param {Array} usernames The usernames of the members that should have access (if using member based protection) - * @param {Int} loginPageId The Id of the login page - * @param {Int} errorPageId The Id of the error page - * @returns {Promise} resourcePromise object containing the public access protection - * - */ - updatePublicAccess: function (contentId, groups, usernames, loginPageId, errorPageId) { - var publicAccess = { - contentId: contentId, - loginPageId: loginPageId, - errorPageId: errorPageId - }; - if (Utilities.isArray(groups) && groups.length) { - publicAccess.groups = groups; - } - else if (Utilities.isArray(usernames) && usernames.length) { - publicAccess.usernames = usernames; - } - else { - throw "must supply either userName/password or roles"; - } - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostPublicAccess", publicAccess) - ), - "Failed to update public access for content item with id " + contentId - ); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#removePublicAccess - * @methodOf umbraco.resources.contentResource - * - * @description - * Removes the public access protection for a content item - * - * ##usage - *
-          * contentResource.removePublicAccess(contentId)
-          *    .then(function() {
-          *        // do your thing
-          *    });
-          * 
- * - * @param {Int} contentId The content Id - * @returns {Promise} resourcePromise object that's resolved once the public access has been removed - * - */ - removePublicAccess: function (contentId) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl("contentApiBaseUrl", "RemovePublicAccess", { - contentId: contentId - }) - ), - "Failed to remove public access for content item with id " + contentId - ); } }; } diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/publicaccess.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/publicaccess.resource.js new file mode 100644 index 000000000000..d91924a2eba3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/resources/publicaccess.resource.js @@ -0,0 +1,128 @@ +/** + * @ngdoc service + * @name umbraco.resources.publicAccessResource + * @description Handles all transactions of public access data + * + * @requires $q + * @requires $http + * @requires umbDataFormatter + * @requires umbRequestHelper + * + **/ + +function publicAccessResource($http, umbRequestHelper) { + + return { + + /** + * @ngdoc method + * @name umbraco.resources.publicAccessResource#getPublicAccess + * @methodOf umbraco.resources.publicAccessResource + * + * @description + * Returns the public access protection for a content item + * + * ##usage + *
+          * publicAccessResource.getPublicAccess(contentId)
+          *    .then(function(publicAccess) {
+          *        // do your thing
+          *    });
+          * 
+ * + * @param {Int} contentId The content Id + * @returns {Promise} resourcePromise object containing the public access protection + * + */ + getPublicAccess: function (contentId) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl("publicAccessApiBaseUrl", "GetPublicAccess", { + contentId: contentId + }) + ), + "Failed to get public access for content item with id " + contentId + ); + }, + + /** + * @ngdoc method + * @name umbraco.resources.publicAccessResource#updatePublicAccess + * @methodOf umbraco.resources.publicAccessResource + * + * @description + * Sets or updates the public access protection for a content item + * + * ##usage + *
+          * publicAccessResource.updatePublicAccess(contentId, userName, password, roles, loginPageId, errorPageId)
+          *    .then(function() {
+          *        // do your thing
+          *    });
+          * 
+ * + * @param {Int} contentId The content Id + * @param {Array} groups The names of the groups that should have access (if using group based protection) + * @param {Array} usernames The usernames of the members that should have access (if using member based protection) + * @param {Int} loginPageId The Id of the login page + * @param {Int} errorPageId The Id of the error page + * @returns {Promise} resourcePromise object containing the public access protection + * + */ + updatePublicAccess: function (contentId, groups, usernames, loginPageId, errorPageId) { + var publicAccess = { + contentId: contentId, + loginPageId: loginPageId, + errorPageId: errorPageId + }; + if (Utilities.isArray(groups) && groups.length) { + publicAccess.groups = groups; + } + else if (Utilities.isArray(usernames) && usernames.length) { + publicAccess.usernames = usernames; + } + else { + throw "must supply either userName/password or roles"; + } + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl("publicAccessApiBaseUrl", "PostPublicAccess", publicAccess) + ), + "Failed to update public access for content item with id " + contentId + ); + }, + + /** + * @ngdoc method + * @name umbraco.resources.publicAccessResource#removePublicAccess + * @methodOf umbraco.resources.publicAccessResource + * + * @description + * Removes the public access protection for a content item + * + * ##usage + *
+          * publicAccessResource.removePublicAccess(contentId)
+          *    .then(function() {
+          *        // do your thing
+          *    });
+          * 
+ * + * @param {Int} contentId The content Id + * @returns {Promise} resourcePromise object that's resolved once the public access has been removed + * + */ + removePublicAccess: function (contentId) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl("publicAccessApiBaseUrl", "RemovePublicAccess", { + contentId: contentId + }) + ), + "Failed to remove public access for content item with id " + contentId + ); + } + }; +} + +angular.module('umbraco.resources').factory('publicAccessResource', publicAccessResource); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js index fcd029484967..01ba2567a7e6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js @@ -1,261 +1,261 @@ (function () { - "use strict"; + "use strict"; - function ContentProtectController($scope, $q, contentResource, memberResource, memberGroupResource, navigationService, localizationService, editorService) { + function ContentProtectController($scope, $q, publicAccessResource, memberResource, memberGroupResource, navigationService, localizationService, editorService) { - var vm = this; - var id = $scope.currentNode.id; + var vm = this; + var id = $scope.currentNode.id; - vm.loading = false; - vm.buttonState = "init"; + vm.loading = false; + vm.buttonState = "init"; - vm.isValid = isValid; - vm.next = next; - vm.save = save; - vm.close = close; - vm.toggle = toggle; - vm.pickLoginPage = pickLoginPage; - vm.pickErrorPage = pickErrorPage; - vm.pickGroup = pickGroup; - vm.removeGroup = removeGroup; - vm.pickMember = pickMember; - vm.removeMember = removeMember; - vm.removeProtection = removeProtection; - vm.removeProtectionConfirm = removeProtectionConfirm; + vm.isValid = isValid; + vm.next = next; + vm.save = save; + vm.close = close; + vm.toggle = toggle; + vm.pickLoginPage = pickLoginPage; + vm.pickErrorPage = pickErrorPage; + vm.pickGroup = pickGroup; + vm.removeGroup = removeGroup; + vm.pickMember = pickMember; + vm.removeMember = removeMember; + vm.removeProtection = removeProtection; + vm.removeProtectionConfirm = removeProtectionConfirm; - vm.type = null; - vm.step = null; + vm.type = null; + vm.step = null; - function onInit() { - vm.loading = true; + function onInit() { + vm.loading = true; - // get the current public access protection - contentResource.getPublicAccess(id).then(function (publicAccess) { - vm.loading = false; + // get the current public access protection + publicAccessResource.getPublicAccess(id).then(function (publicAccess) { + vm.loading = false; - // init the current settings for public access (if any) - vm.loginPage = publicAccess.loginPage; - vm.errorPage = publicAccess.errorPage; - vm.groups = publicAccess.groups || []; - vm.members = publicAccess.members || []; - vm.canRemove = true; + // init the current settings for public access (if any) + vm.loginPage = publicAccess.loginPage; + vm.errorPage = publicAccess.errorPage; + vm.groups = publicAccess.groups || []; + vm.members = publicAccess.members || []; + vm.canRemove = true; - if (vm.members.length) { - vm.type = "member"; - next(); - } - else if (vm.groups.length) { - vm.type = "group"; - next(); - } - else { - vm.canRemove = false; - } - }); + if (vm.members.length) { + vm.type = "member"; + next(); } - - function next() { - if (vm.type === "group") { - vm.loading = true; - // get all existing member groups for lookup upon selection - // NOTE: if/when member groups support infinite editing, we can't rely on using a cached lookup list of valid groups anymore - memberGroupResource.getGroups().then(function (groups) { - vm.step = vm.type; - vm.allGroups = groups; - vm.hasGroups = groups.length > 0; - vm.loading = false; - }); - } - else { - vm.step = vm.type; - } + else if (vm.groups.length) { + vm.type = "group"; + next(); } - - function isValid() { - if (!vm.type) { - return false; - } - if (!vm.protectForm.$valid) { - return false; - } - if (!vm.loginPage || !vm.errorPage) { - return false; - } - if (vm.type === "group") { - return vm.groups && vm.groups.length > 0; - } - if (vm.type === "member") { - return vm.members && vm.members.length > 0; - } - return true; + else { + vm.canRemove = false; } + }); + } - function save() { - vm.buttonState = "busy"; - var groups = _.map(vm.groups, function (group) { return group.name; }); - var usernames = _.map(vm.members, function (member) { return member.username; }); - contentResource.updatePublicAccess(id, groups, usernames, vm.loginPage.id, vm.errorPage.id).then( - function () { - localizationService.localize("publicAccess_paIsProtected", [$scope.currentNode.name]).then(function (value) { - vm.success = { - message: value - }; - }); - navigationService.syncTree({ tree: "content", path: $scope.currentNode.path, forceReload: true }); - $scope.dialog.confirmDiscardChanges = true; - }, function (error) { - vm.error = error; - vm.buttonState = "error"; - } - ); - } + function next() { + if (vm.type === "group") { + vm.loading = true; + // get all existing member groups for lookup upon selection + // NOTE: if/when member groups support infinite editing, we can't rely on using a cached lookup list of valid groups anymore + memberGroupResource.getGroups().then(function (groups) { + vm.step = vm.type; + vm.allGroups = groups; + vm.hasGroups = groups.length > 0; + vm.loading = false; + }); + } + else { + vm.step = vm.type; + } + } - function close() { - // ensure that we haven't set a locked state on the dialog before closing it - navigationService.allowHideDialog(true); - navigationService.hideDialog(); - } + function isValid() { + if (!vm.type) { + return false; + } + if (!vm.protectForm.$valid) { + return false; + } + if (!vm.loginPage || !vm.errorPage) { + return false; + } + if (vm.type === "group") { + return vm.groups && vm.groups.length > 0; + } + if (vm.type === "member") { + return vm.members && vm.members.length > 0; + } + return true; + } - function toggle(group) { - group.selected = !group.selected; - $scope.dialog.confirmDiscardChanges = true; + function save() { + vm.buttonState = "busy"; + var groups = _.map(vm.groups, function (group) { return group.name; }); + var usernames = _.map(vm.members, function (member) { return member.username; }); + publicAccessResource.updatePublicAccess(id, groups, usernames, vm.loginPage.id, vm.errorPage.id).then( + function () { + localizationService.localize("publicAccess_paIsProtected", [$scope.currentNode.name]).then(function (value) { + vm.success = { + message: value + }; + }); + navigationService.syncTree({ tree: "content", path: $scope.currentNode.path, forceReload: true }); + $scope.dialog.confirmDiscardChanges = true; + }, function (error) { + vm.error = error; + vm.buttonState = "error"; } + ); + } + + function close() { + // ensure that we haven't set a locked state on the dialog before closing it + navigationService.allowHideDialog(true); + navigationService.hideDialog(); + } + + function toggle(group) { + group.selected = !group.selected; + $scope.dialog.confirmDiscardChanges = true; + } - function pickGroup() { - navigationService.allowHideDialog(false); - editorService.memberGroupPicker({ - multiPicker: true, - submit: function(model) { - var selectedGroupIds = model.selectedMemberGroups - ? model.selectedMemberGroups - : [model.selectedMemberGroup]; - _.each(selectedGroupIds, - function (groupId) { - // find the group in the lookup list and add it if it isn't already - var group = _.find(vm.allGroups, function(g) { return g.id === parseInt(groupId); }); - if (group && !_.find(vm.groups, function (g) { return g.id === group.id })) { - vm.groups.push(group); - } - }); - editorService.close(); - navigationService.allowHideDialog(true); - $scope.dialog.confirmDiscardChanges = true; - }, - close: function() { - editorService.close(); - navigationService.allowHideDialog(true); - } + function pickGroup() { + navigationService.allowHideDialog(false); + editorService.memberGroupPicker({ + multiPicker: true, + submit: function (model) { + var selectedGroupIds = model.selectedMemberGroups + ? model.selectedMemberGroups + : [model.selectedMemberGroup]; + _.each(selectedGroupIds, + function (groupId) { + // find the group in the lookup list and add it if it isn't already + var group = _.find(vm.allGroups, function (g) { return g.id === parseInt(groupId); }); + if (group && !_.find(vm.groups, function (g) { return g.id === group.id })) { + vm.groups.push(group); + } }); + editorService.close(); + navigationService.allowHideDialog(true); + $scope.dialog.confirmDiscardChanges = true; + }, + close: function () { + editorService.close(); + navigationService.allowHideDialog(true); } + }); + } - function removeGroup(group) { - vm.groups = _.reject(vm.groups, function(g) { return g.id === group.id }); - $scope.dialog.confirmDiscardChanges = true; - } + function removeGroup(group) { + vm.groups = _.reject(vm.groups, function (g) { return g.id === group.id }); + $scope.dialog.confirmDiscardChanges = true; + } - function pickMember() { - navigationService.allowHideDialog(false); - // TODO: once editorService has a memberPicker method, use that instead - editorService.treePicker({ - multiPicker: true, - entityType: "Member", - section: "member", - treeAlias: "member", - filter: function (i) { - return i.metaData.isContainer; - }, - filterCssClass: "not-allowed", - submit: function (model) { - if (model.selection && model.selection.length) { - var promises = []; - // get the selected member usernames - _.each(model.selection, - function (member) { - // TODO: - // as-is we need to fetch all the picked members one at a time to get their usernames. - // when editorService has a memberPicker method, see if this can't be avoided - otherwise - // add a memberResource.getByKeys() method to do all this in one request - promises.push( - memberResource.getByKey(member.key).then(function(newMember) { - if (!_.find(vm.members, function (currentMember) { return currentMember.username === newMember.username })) { - vm.members.push(newMember); - } - }) - ); - }); - editorService.close(); - navigationService.allowHideDialog(true); - // wait for all the member lookups to complete - vm.loading = true; - $q.all(promises).then(function() { - vm.loading = false; - }); - $scope.dialog.confirmDiscardChanges = true; + function pickMember() { + navigationService.allowHideDialog(false); + // TODO: once editorService has a memberPicker method, use that instead + editorService.treePicker({ + multiPicker: true, + entityType: "Member", + section: "member", + treeAlias: "member", + filter: function (i) { + return i.metaData.isContainer; + }, + filterCssClass: "not-allowed", + submit: function (model) { + if (model.selection && model.selection.length) { + var promises = []; + // get the selected member usernames + _.each(model.selection, + function (member) { + // TODO: + // as-is we need to fetch all the picked members one at a time to get their usernames. + // when editorService has a memberPicker method, see if this can't be avoided - otherwise + // add a memberResource.getByKeys() method to do all this in one request + promises.push( + memberResource.getByKey(member.key).then(function (newMember) { + if (!_.find(vm.members, function (currentMember) { return currentMember.username === newMember.username })) { + vm.members.push(newMember); } - }, - close: function () { - editorService.close(); - navigationService.allowHideDialog(true); - } + }) + ); + }); + editorService.close(); + navigationService.allowHideDialog(true); + // wait for all the member lookups to complete + vm.loading = true; + $q.all(promises).then(function () { + vm.loading = false; }); + $scope.dialog.confirmDiscardChanges = true; + } + }, + close: function () { + editorService.close(); + navigationService.allowHideDialog(true); } + }); + } - function removeMember(member) { - vm.members = _.without(vm.members, member); - } + function removeMember(member) { + vm.members = _.without(vm.members, member); + } - function pickLoginPage() { - pickPage(vm.loginPage); - } + function pickLoginPage() { + pickPage(vm.loginPage); + } - function pickErrorPage() { - pickPage(vm.errorPage); - } + function pickErrorPage() { + pickPage(vm.errorPage); + } - function pickPage(page) { - navigationService.allowHideDialog(false); - editorService.contentPicker({ - submit: function (model) { - if (page === vm.loginPage) { - vm.loginPage = model.selection[0]; - } - else { - vm.errorPage = model.selection[0]; - } - editorService.close(); - navigationService.allowHideDialog(true); - $scope.dialog.confirmDiscardChanges = true; - }, - close: function () { - editorService.close(); - navigationService.allowHideDialog(true); - } - }); + function pickPage(page) { + navigationService.allowHideDialog(false); + editorService.contentPicker({ + submit: function (model) { + if (page === vm.loginPage) { + vm.loginPage = model.selection[0]; + } + else { + vm.errorPage = model.selection[0]; + } + editorService.close(); + navigationService.allowHideDialog(true); + $scope.dialog.confirmDiscardChanges = true; + }, + close: function () { + editorService.close(); + navigationService.allowHideDialog(true); } + }); + } - function removeProtection() { - vm.removing = true; - } + function removeProtection() { + vm.removing = true; + } - function removeProtectionConfirm() { - vm.buttonState = "busy"; - contentResource.removePublicAccess(id).then( - function () { - localizationService.localize("publicAccess_paIsRemoved", [$scope.currentNode.name]).then(function(value) { - vm.success = { - message: value - }; - }); - navigationService.syncTree({ tree: "content", path: $scope.currentNode.path, forceReload: true }); - }, function (error) { - vm.error = error; - vm.buttonState = "error"; - } - ); + function removeProtectionConfirm() { + vm.buttonState = "busy"; + publicAccessResource.removePublicAccess(id).then( + function () { + localizationService.localize("publicAccess_paIsRemoved", [$scope.currentNode.name]).then(function (value) { + vm.success = { + message: value + }; + }); + navigationService.syncTree({ tree: "content", path: $scope.currentNode.path, forceReload: true }); + }, function (error) { + vm.error = error; + vm.buttonState = "error"; } - - onInit(); + ); } - angular.module("umbraco").controller("Umbraco.Editors.Content.ProtectController", ContentProtectController); + onInit(); + } + + angular.module("umbraco").controller("Umbraco.Editors.Content.ProtectController", ContentProtectController); })(); From d4382be02f7eec9a6bfdca3279d5328f9744e6c2 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 14 Apr 2021 16:35:51 +1000 Subject: [PATCH 05/26] Implements the required methods on IMemberManager, removes old migrated code --- .../PublishedCache/IPublishedMemberCache.cs | 3 +- .../Services/PublicAccessServiceExtensions.cs | 5 +- .../Security/IMemberManager.cs | 34 ++- .../Security/MemberManagerTests.cs | 4 +- .../Filters/UmbracoMemberAuthorizeFilter.cs | 11 +- .../Security/MemberManager.cs | 204 +++++++++++++++- .../EnsurePublishedContentRequestAttribute.cs | 0 .../Mvc/MemberAuthorizeAttribute.cs | 64 ----- src/Umbraco.Web/Security/MembershipHelper.cs | 226 ------------------ src/Umbraco.Web/Umbraco.Web.csproj | 2 - src/Umbraco.Web/UmbracoHelper.cs | 10 +- .../WebApi/MemberAuthorizeAttribute.cs | 52 ---- 12 files changed, 246 insertions(+), 369 deletions(-) delete mode 100644 src/Umbraco.Web/Mvc/EnsurePublishedContentRequestAttribute.cs delete mode 100644 src/Umbraco.Web/Mvc/MemberAuthorizeAttribute.cs delete mode 100644 src/Umbraco.Web/WebApi/MemberAuthorizeAttribute.cs diff --git a/src/Umbraco.Core/PublishedCache/IPublishedMemberCache.cs b/src/Umbraco.Core/PublishedCache/IPublishedMemberCache.cs index 67036202ddcb..8a7efdeff31e 100644 --- a/src/Umbraco.Core/PublishedCache/IPublishedMemberCache.cs +++ b/src/Umbraco.Core/PublishedCache/IPublishedMemberCache.cs @@ -1,9 +1,10 @@ -using System.Xml.XPath; +using System.Xml.XPath; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; namespace Umbraco.Cms.Core.PublishedCache { + // TODO: Kill this, why do we want this at all? public interface IPublishedMemberCache : IXPathNavigable { IPublishedContent GetByProviderKey(object key); diff --git a/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs b/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs index 44e9d61fee8c..487a0ad50afe 100644 --- a/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs +++ b/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; @@ -64,7 +65,7 @@ public static bool HasAccess(this IPublicAccessService publicAccessService, int /// /// A callback to retrieve the roles for this member /// - public static bool HasAccess(this IPublicAccessService publicAccessService, string path, string username, Func> rolesCallback) + public static async Task HasAccessAsync(this IPublicAccessService publicAccessService, string path, string username, Func>> rolesCallback) { if (rolesCallback == null) throw new ArgumentNullException("roles"); if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", "username"); @@ -73,7 +74,7 @@ public static bool HasAccess(this IPublicAccessService publicAccessService, stri var entry = publicAccessService.GetEntryForContent(path.EnsureEndsWith(path)); if (entry == null) return true; - var roles = rolesCallback(username); + var roles = await rolesCallback(); return HasAccess(entry, username, roles); } diff --git a/src/Umbraco.Infrastructure/Security/IMemberManager.cs b/src/Umbraco.Infrastructure/Security/IMemberManager.cs index 081b8cd6c969..e185c2a0ae9c 100644 --- a/src/Umbraco.Infrastructure/Security/IMemberManager.cs +++ b/src/Umbraco.Infrastructure/Security/IMemberManager.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading.Tasks; namespace Umbraco.Cms.Core.Security { @@ -14,15 +15,38 @@ public interface IMemberManager : IUmbracoUserManager /// Allowed groups. /// Allowed individual members. /// True or false if the currently logged in member is authorized - bool IsMemberAuthorized( + Task IsMemberAuthorizedAsync( IEnumerable allowTypes = null, IEnumerable allowGroups = null, IEnumerable allowMembers = null); - // TODO: We'll need to add some additional things here that people will be using in their code: + /// + /// Check if a member is logged in + /// + /// + bool IsLoggedIn(); + + /// + /// Check if the current user has access to a document + /// + /// The full path of the document object to check + /// True if the current user has access or if the current document isn't protected + Task MemberHasAccessAsync(string path); + + /// + /// Checks if the current user has access to the paths + /// + /// + /// + Task> MemberHasAccessAsync(IEnumerable paths); + + /// + /// Check if a document object is protected by the "Protect Pages" functionality in umbraco + /// + /// The full path of the document object to check + /// True if the document object is protected + Task IsProtectedAsync(string path); - // bool MemberHasAccess(string path); - // IReadOnlyDictionary MemberHasAccess(IEnumerable paths) - // Possibly some others from the old MembershipHelper + Task> IsProtectedAsync(IEnumerable paths); } } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs index e184263141e9..0f4e2c920a06 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs @@ -73,7 +73,9 @@ public MemberManager CreateSut() new BackOfficeIdentityErrorDescriber(), _mockServiceProviders.Object, new Mock>>().Object, - _mockPasswordConfiguration.Object); + _mockPasswordConfiguration.Object, + Mock.Of(), + Mock.Of()); validator.Setup(v => v.ValidateAsync( userManager, diff --git a/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs b/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs index 5ad064cd377b..b0a640b2301b 100644 --- a/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs +++ b/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; @@ -11,7 +12,7 @@ namespace Umbraco.Cms.Web.Common.Filters /// /// Ensures authorization is successful for a front-end member /// - public class UmbracoMemberAuthorizeFilter : IAuthorizationFilter + public class UmbracoMemberAuthorizeFilter : IAsyncAuthorizationFilter { public UmbracoMemberAuthorizeFilter() { @@ -39,18 +40,18 @@ public UmbracoMemberAuthorizeFilter(string allowType, string allowGroup, string ///
public string AllowMembers { get; private set; } - public void OnAuthorization(AuthorizationFilterContext context) + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) { IMemberManager memberManager = context.HttpContext.RequestServices.GetRequiredService(); - if (!IsAuthorized(memberManager)) + if (!await IsAuthorizedAsync(memberManager)) { context.HttpContext.SetReasonPhrase("Resource restricted: either member is not logged on or is not of a permitted type or group."); context.Result = new ForbidResult(); } } - private bool IsAuthorized(IMemberManager memberManager) + private async Task IsAuthorizedAsync(IMemberManager memberManager) { if (AllowMembers.IsNullOrWhiteSpace()) { @@ -76,7 +77,7 @@ private bool IsAuthorized(IMemberManager memberManager) } } - return memberManager.IsMemberAuthorized(AllowType.Split(Core.Constants.CharArrays.Comma), AllowGroup.Split(Core.Constants.CharArrays.Comma), members); + return await memberManager.IsMemberAuthorizedAsync(AllowType.Split(Core.Constants.CharArrays.Comma), AllowGroup.Split(Core.Constants.CharArrays.Comma), members); } } } diff --git a/src/Umbraco.Web.Common/Security/MemberManager.cs b/src/Umbraco.Web.Common/Security/MemberManager.cs index b1967c74cf66..5a5b1414b547 100644 --- a/src/Umbraco.Web.Common/Security/MemberManager.cs +++ b/src/Umbraco.Web.Common/Security/MemberManager.cs @@ -1,17 +1,26 @@ using System; +using System.Linq; using System.Collections.Generic; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Umbraco.Extensions; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Net; using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using System.Threading.Tasks; namespace Umbraco.Cms.Web.Common.Security { public class MemberManager : UmbracoUserManager, IMemberManager { + private readonly IPublicAccessService _publicAccessService; + private readonly IHttpContextAccessor _httpContextAccessor; + private MemberIdentityUser _currentMember; + public MemberManager( IIpResolver ipResolver, IUserStore store, @@ -22,13 +31,202 @@ public MemberManager( BackOfficeIdentityErrorDescriber errors, IServiceProvider services, ILogger> logger, - IOptions passwordConfiguration) + IOptions passwordConfiguration, + IPublicAccessService publicAccessService, + IHttpContextAccessor httpContextAccessor) : base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, errors, services, logger, passwordConfiguration) { + _publicAccessService = publicAccessService; + _httpContextAccessor = httpContextAccessor; + } + + /// + public async Task IsMemberAuthorizedAsync(IEnumerable allowTypes = null, IEnumerable allowGroups = null, IEnumerable allowMembers = null) + { + if (allowTypes == null) + { + allowTypes = Enumerable.Empty(); + } + + if (allowGroups == null) + { + allowGroups = Enumerable.Empty(); + } + + if (allowMembers == null) + { + allowMembers = Enumerable.Empty(); + } + + // Allow by default + var allowAction = true; + + if (IsLoggedIn() == false) + { + // If not logged on, not allowed + allowAction = false; + } + else + { + string username; + + MemberIdentityUser currentMember = await GetCurrentMemberAsync(); + + // If a member could not be resolved from the provider, we are clearly not authorized and can break right here + if (currentMember == null) + { + return false; + } + + int memberId = int.Parse(currentMember.Id); + username = currentMember.UserName; + + // If types defined, check member is of one of those types + IList allowTypesList = allowTypes as IList ?? allowTypes.ToList(); + if (allowTypesList.Any(allowType => allowType != string.Empty)) + { + // Allow only if member's type is in list + allowAction = allowTypesList.Select(x => x.ToLowerInvariant()).Contains(currentMember.MemberTypeAlias.ToLowerInvariant()); + } + + // If specific members defined, check member is of one of those + if (allowAction && allowMembers.Any()) + { + // Allow only if member's Id is in the list + allowAction = allowMembers.Contains(memberId); + } + + // If groups defined, check member is of one of those groups + IList allowGroupsList = allowGroups as IList ?? allowGroups.ToList(); + if (allowAction && allowGroupsList.Any(allowGroup => allowGroup != string.Empty)) + { + // Allow only if member is assigned to a group in the list + IList groups = await GetRolesAsync(currentMember); + allowAction = allowGroupsList.Select(s => s.ToLowerInvariant()).Intersect(groups.Select(myGroup => myGroup.ToLowerInvariant())).Any(); + } + } + + return allowAction; + } + + /// + public bool IsLoggedIn() + { + HttpContext httpContext = _httpContextAccessor.HttpContext; + return httpContext?.User != null && httpContext.User.Identity.IsAuthenticated; } - public bool IsMemberAuthorized(IEnumerable allowTypes = null, IEnumerable allowGroups = null, IEnumerable allowMembers = null) - => throw new NotImplementedException(); // TODO: Implement! + /// + public async Task MemberHasAccessAsync(string path) + { + if (await IsProtectedAsync(path)) + { + return await HasAccessAsync(path); + } + return true; + } + + /// + public async Task> MemberHasAccessAsync(IEnumerable paths) + { + IReadOnlyDictionary protectedPaths = await IsProtectedAsync(paths); + + IEnumerable pathsWithProtection = protectedPaths.Where(x => x.Value).Select(x => x.Key); + IReadOnlyDictionary pathsWithAccess = await HasAccessAsync(pathsWithProtection); + + var result = new Dictionary(); + foreach (var path in paths) + { + pathsWithAccess.TryGetValue(path, out var hasAccess); + // if it's not found it's false anyways + result[path] = !pathsWithProtection.Contains(path) || hasAccess; + } + return result; + } + + /// + /// + /// this is a cached call + /// + public Task IsProtectedAsync(string path) => Task.FromResult(_publicAccessService.IsProtected(path).Success); + + /// + public Task> IsProtectedAsync(IEnumerable paths) + { + var result = new Dictionary(); + foreach (var path in paths) + { + //this is a cached call + result[path] = _publicAccessService.IsProtected(path); + } + return Task.FromResult((IReadOnlyDictionary)result); + } + + /// + /// This will check if the member has access to this path + /// + /// + /// + /// + private async Task HasAccessAsync(string path) + { + MemberIdentityUser currentMember = await GetCurrentMemberAsync(); + if (currentMember == null) + { + return false; + } + + return await _publicAccessService.HasAccessAsync( + path, + currentMember.UserName, + async () => await GetRolesAsync(currentMember)); + } + + private async Task> HasAccessAsync(IEnumerable paths) + { + var result = new Dictionary(); + MemberIdentityUser currentMember = await GetCurrentMemberAsync(); + + if (currentMember == null) + { + return result; + } + + // ensure we only lookup user roles once + IList userRoles = null; + async Task> getUserRolesAsync() + { + if (userRoles != null) + { + return userRoles; + } + + userRoles = await GetRolesAsync(currentMember); + return userRoles; + } + + foreach (var path in paths) + { + result[path] = await _publicAccessService.HasAccessAsync( + path, + currentMember.UserName, + async () => await getUserRolesAsync()); + } + return result; + } + + private async Task GetCurrentMemberAsync() + { + if (_currentMember == null) + { + if (!IsLoggedIn()) + { + return null; + } + _currentMember = await GetUserAsync(_httpContextAccessor.HttpContext.User); + } + return _currentMember; + } } } diff --git a/src/Umbraco.Web/Mvc/EnsurePublishedContentRequestAttribute.cs b/src/Umbraco.Web/Mvc/EnsurePublishedContentRequestAttribute.cs deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/Umbraco.Web/Mvc/MemberAuthorizeAttribute.cs b/src/Umbraco.Web/Mvc/MemberAuthorizeAttribute.cs deleted file mode 100644 index 6395a0d19301..000000000000 --- a/src/Umbraco.Web/Mvc/MemberAuthorizeAttribute.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Collections.Generic; -using System.Web; -using System.Web.Mvc; -using Umbraco.Extensions; -using AuthorizeAttribute = System.Web.Mvc.AuthorizeAttribute; -using Current = Umbraco.Web.Composing.Current; - -namespace Umbraco.Web.Mvc -{ - /// - /// Attribute for attributing controller actions to restrict them - /// to just authenticated members, and optionally of a particular type and/or group - /// - public sealed class MemberAuthorizeAttribute : AuthorizeAttribute - { - /// - /// Comma delimited list of allowed member types - /// - public string AllowType { get; set; } - - /// - /// Comma delimited list of allowed member groups - /// - public string AllowGroup { get; set; } - - /// - /// Comma delimited list of allowed members - /// - public string AllowMembers { get; set; } - - protected override bool AuthorizeCore(HttpContextBase httpContext) - { - if (AllowMembers.IsNullOrWhiteSpace()) - AllowMembers = ""; - if (AllowGroup.IsNullOrWhiteSpace()) - AllowGroup = ""; - if (AllowType.IsNullOrWhiteSpace()) - AllowType = ""; - - var members = new List(); - foreach (var s in AllowMembers.Split(',')) - { - if (int.TryParse(s, out var id)) - { - members.Add(id); - } - } - - var helper = Current.MembershipHelper; - return helper.IsMemberAuthorized(AllowType.Split(','), AllowGroup.Split(','), members); - - } - - /// - /// Override method to throw exception instead of returning a 401 result - /// - /// - protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) - { - throw new HttpException(403, "Resource restricted: either member is not logged on or is not of a permitted type or group."); - } - - } -} diff --git a/src/Umbraco.Web/Security/MembershipHelper.cs b/src/Umbraco.Web/Security/MembershipHelper.cs index e93fcbdfd05f..61ed6c9fddbb 100644 --- a/src/Umbraco.Web/Security/MembershipHelper.cs +++ b/src/Umbraco.Web/Security/MembershipHelper.cs @@ -17,15 +17,6 @@ namespace Umbraco.Web.Security { // MIGRATED TO NETCORE - // TODO: Analyse all - much can be moved/removed since most methods will occur on the manager via identity implementation - - /// - /// Helper class containing logic relating to the built-in Umbraco members macros and controllers for: - /// - Registration - /// - Updating - /// - Logging in - /// - Current status - /// public class MembershipHelper { private readonly MembersMembershipProvider _membershipProvider; @@ -76,95 +67,6 @@ IEntityService entityService protected IPublishedMemberCache MemberCache { get; } - /// - /// Check if a document object is protected by the "Protect Pages" functionality in umbraco - /// - /// The full path of the document object to check - /// True if the document object is protected - public virtual bool IsProtected(string path) - { - //this is a cached call - return _publicAccessService.IsProtected(path); - } - - public virtual IDictionary IsProtected(IEnumerable paths) - { - var result = new Dictionary(); - foreach (var path in paths) - { - //this is a cached call - result[path] = _publicAccessService.IsProtected(path); - } - return result; - } - - /// - /// Check if the current user has access to a document - /// - /// The full path of the document object to check - /// True if the current user has access or if the current document isn't protected - public virtual bool MemberHasAccess(string path) - { - if (IsProtected(path)) - { - return IsLoggedIn() && HasAccess(path, Roles.Provider); - } - return true; - } - - /// - /// Checks if the current user has access to the paths - /// - /// - /// - public virtual IDictionary MemberHasAccess(IEnumerable paths) - { - var protectedPaths = IsProtected(paths); - - var pathsWithProtection = protectedPaths.Where(x => x.Value).Select(x => x.Key); - var pathsWithAccess = HasAccess(pathsWithProtection, Roles.Provider); - - var result = new Dictionary(); - foreach (var path in paths) - { - pathsWithAccess.TryGetValue(path, out var hasAccess); - // if it's not found it's false anyways - result[path] = !pathsWithProtection.Contains(path) || hasAccess; - } - return result; - } - - /// - /// This will check if the member has access to this path - /// - /// - /// - /// - private bool HasAccess(string path, RoleProvider roleProvider) - { - return _publicAccessService.HasAccess(path, CurrentUserName, roleProvider.GetRolesForUser); - } - - private IDictionary HasAccess(IEnumerable paths, RoleProvider roleProvider) - { - // ensure we only lookup user roles once - string[] userRoles = null; - string[] getUserRoles(string username) - { - if (userRoles != null) - return userRoles; - userRoles = roleProvider.GetRolesForUser(username).ToArray(); - return userRoles; - } - - var result = new Dictionary(); - foreach (var path in paths) - { - result[path] = IsLoggedIn() && _publicAccessService.HasAccess(path, CurrentUserName, getUserRoles); - } - return result; - } - #region Querying for front-end public virtual IPublishedContent GetByProviderKey(object key) @@ -228,135 +130,7 @@ public virtual IPublishedContent Get(Udi udi) return null; } - /// - /// Returns the currently logged in member as IPublishedContent - /// - /// - public virtual IPublishedContent GetCurrentMember() - { - if (IsLoggedIn() == false) - { - return null; - } - var result = GetCurrentPersistedMember(); - return result == null ? null : MemberCache.GetByMember(result); - } - #endregion - /// - /// Gets the current user's roles. - /// - /// Roles are cached per user name, at request level. - public IEnumerable GetCurrentUserRoles() - => GetUserRoles(CurrentUserName); - - /// - /// Gets a user's roles. - /// - /// Roles are cached per user name, at request level. - public IEnumerable GetUserRoles(string userName) - { - // optimize by caching per-request (v7 cached per PublishedRequest, in PublishedRouter) - var key = "Umbraco.Web.Security.MembershipHelper__Roles__" + userName; - return _appCaches.RequestCache.GetCacheItem(key, () => Roles.Provider.GetRolesForUser(userName)); - } - - /// - /// Check if a member is logged in - /// - /// - public bool IsLoggedIn() - { - var httpContext = _httpContextAccessor.HttpContext; - return httpContext?.User != null && httpContext.User.Identity.IsAuthenticated; - } - - /// - /// Returns the currently logged in username - /// - public string CurrentUserName => _httpContextAccessor.GetRequiredHttpContext().User.Identity.Name; - - /// - /// Returns true or false if the currently logged in member is authorized based on the parameters provided - /// - /// - /// - /// - /// - public virtual bool IsMemberAuthorized( - IEnumerable allowTypes = null, - IEnumerable allowGroups = null, - IEnumerable allowMembers = null) - { - if (allowTypes == null) - allowTypes = Enumerable.Empty(); - if (allowGroups == null) - allowGroups = Enumerable.Empty(); - if (allowMembers == null) - allowMembers = Enumerable.Empty(); - - // Allow by default - var allowAction = true; - - if (IsLoggedIn() == false) - { - // If not logged on, not allowed - allowAction = false; - } - else - { - var provider = _membershipProvider; - - string username; - - var member = GetCurrentPersistedMember(); - // If a member could not be resolved from the provider, we are clearly not authorized and can break right here - if (member == null) - return false; - username = member.Username; - - // If types defined, check member is of one of those types - var allowTypesList = allowTypes as IList ?? allowTypes.ToList(); - if (allowTypesList.Any(allowType => allowType != string.Empty)) - { - // Allow only if member's type is in list - allowAction = allowTypesList.Select(x => x.ToLowerInvariant()).Contains(member.ContentType.Alias.ToLowerInvariant()); - } - - // If specific members defined, check member is of one of those - if (allowAction && allowMembers.Any()) - { - // Allow only if member's Id is in the list - allowAction = allowMembers.Contains(member.Id); - } - - // If groups defined, check member is of one of those groups - var allowGroupsList = allowGroups as IList ?? allowGroups.ToList(); - if (allowAction && allowGroupsList.Any(allowGroup => allowGroup != string.Empty)) - { - // Allow only if member is assigned to a group in the list - var groups = _roleProvider.GetRolesForUser(username); - allowAction = allowGroupsList.Select(s => s.ToLowerInvariant()).Intersect(groups.Select(myGroup => myGroup.ToLowerInvariant())).Any(); - } - } - - return allowAction; - } - - /// - /// Returns the currently logged in IMember object - this should never be exposed to the front-end since it's returning a business logic entity! - /// - /// - private IMember GetCurrentPersistedMember() - { - var provider = _membershipProvider; - - var username = provider.GetCurrentUserName(); - // The result of this is cached by the MemberRepository - var member = _memberService.GetByUsername(username); - return member; - } - } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index ba1e4efe7300..60299a9b34aa 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -137,7 +137,6 @@ - @@ -182,7 +181,6 @@ - diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index d396aee2f0f8..f7f6374ea470 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -189,19 +189,13 @@ public string GetDictionaryValue(string key, string altText) ///
/// The full path of the document object to check /// True if the current user has access or if the current document isn't protected - public bool MemberHasAccess(string path) - { - return _membershipHelper.MemberHasAccess(path); - } + public bool MemberHasAccess(string path) => throw new NotImplementedException("Migrated to netcore"); /// /// Whether or not the current member is logged in (based on the membership provider) /// /// True is the current user is logged in - public bool MemberIsLoggedOn() - { - return _membershipHelper.IsLoggedIn(); - } + public bool MemberIsLoggedOn() => throw new NotImplementedException("Migrated to netcore"); #endregion diff --git a/src/Umbraco.Web/WebApi/MemberAuthorizeAttribute.cs b/src/Umbraco.Web/WebApi/MemberAuthorizeAttribute.cs deleted file mode 100644 index d91f164cf2a6..000000000000 --- a/src/Umbraco.Web/WebApi/MemberAuthorizeAttribute.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Collections.Generic; -using System.Web.Http; -using Umbraco.Extensions; -using Current = Umbraco.Web.Composing.Current; - -namespace Umbraco.Web.WebApi -{ - /// - /// Attribute for attributing controller actions to restrict them - /// to just authenticated members, and optionally of a particular type and/or group - /// - public sealed class MemberAuthorizeAttribute : AuthorizeAttribute - { - /// - /// Comma delimited list of allowed member types - /// - public string AllowType { get; set; } - - /// - /// Comma delimited list of allowed member groups - /// - public string AllowGroup { get; set; } - - /// - /// Comma delimited list of allowed members - /// - public string AllowMembers { get; set; } - - protected override bool IsAuthorized(System.Web.Http.Controllers.HttpActionContext actionContext) - { - if (AllowMembers.IsNullOrWhiteSpace()) - AllowMembers = ""; - if (AllowGroup.IsNullOrWhiteSpace()) - AllowGroup = ""; - if (AllowType.IsNullOrWhiteSpace()) - AllowType = ""; - - var members = new List(); - foreach (var s in AllowMembers.Split(',')) - { - if (int.TryParse(s, out var id)) - { - members.Add(id); - } - } - - var helper = Current.MembershipHelper; - return helper.IsMemberAuthorized(AllowType.Split(','), AllowGroup.Split(','), members); - } - - } -} From 2907ac7927f7859f8637e8d49e9fd067b802db97 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 15 Apr 2021 21:16:17 +1000 Subject: [PATCH 06/26] Updates routing to be able to re-route, Fixes middleware ordering ensuring endpoints are last, refactors pipeline options, adds public access middleware, ensures public access follows all hops --- src/Umbraco.Core/Routing/IPublishedRouter.cs | 25 ++- src/Umbraco.Core/Routing/PublishedRouter.cs | 212 ++++++------------ .../Security/IMemberManager.cs | 6 + .../UmbracoTestServerTestBase.cs | 4 +- .../Routing/UmbracoRouteValuesFactoryTests.cs | 8 +- src/Umbraco.Tests/TestHelpers/BaseWebTest.cs | 2 - .../UmbracoBuilderExtensions.cs | 5 +- .../BackOfficeApplicationBuilderExtensions.cs | 54 ----- .../UmbracoApplicationBuilder.BackOffice.cs | 40 ++++ .../UmbracoApplicationBuilder.Preview.cs | 24 ++ ...iceExternalLoginProviderErrorMiddleware.cs | 18 +- .../IUmbracoApplicationBuilder.cs | 16 ++ .../IUmbracoPipelineFilter.cs | 39 ++++ .../UmbracoApplicationBuilder.cs | 17 ++ .../UmbracoPipelineFilter.cs | 37 +++ .../UmbracoPipelineOptions.cs | 22 ++ .../Controllers/RenderController.cs | 3 +- .../UmbracoApplicationServicesCapture.cs | 20 ++ .../UmbracoBuilderExtensions.cs | 2 +- .../UmbracoStartupFilter.cs | 29 --- .../UmbracoStartupFilterOptions.cs | 23 -- .../ApplicationBuilderExtensions.cs | 67 +++--- ...=> UmbracoApplicationBuilder.Installer.cs} | 11 +- ...oApplicationBuilder.RuntimeMinification.cs | 32 +++ .../Security/MemberManager.cs | 33 +-- src/Umbraco.Web.UI.NetCore/Startup.cs | 18 +- .../UmbracoBuilderExtensions.cs | 8 +- ...s => UmbracoApplicationBuilder.Website.cs} | 23 +- .../Middleware/PublicAccessMiddleware.cs | 171 ++++++++++++++ .../Routing/UmbracoRouteValueTransformer.cs | 5 +- .../Routing/UmbracoRouteValuesFactory.cs | 9 +- 31 files changed, 637 insertions(+), 346 deletions(-) delete mode 100644 src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs create mode 100644 src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs create mode 100644 src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.Preview.cs create mode 100644 src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilder.cs create mode 100644 src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoPipelineFilter.cs create mode 100644 src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs create mode 100644 src/Umbraco.Web.Common/ApplicationBuilder/UmbracoPipelineFilter.cs create mode 100644 src/Umbraco.Web.Common/ApplicationBuilder/UmbracoPipelineOptions.cs create mode 100644 src/Umbraco.Web.Common/DependencyInjection/UmbracoApplicationServicesCapture.cs delete mode 100644 src/Umbraco.Web.Common/DependencyInjection/UmbracoStartupFilter.cs delete mode 100644 src/Umbraco.Web.Common/DependencyInjection/UmbracoStartupFilterOptions.cs rename src/Umbraco.Web.Common/Extensions/{UmbracoInstallApplicationBuilderExtensions.cs => UmbracoApplicationBuilder.Installer.cs} (54%) create mode 100644 src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.RuntimeMinification.cs rename src/Umbraco.Web.Website/Extensions/{UmbracoWebsiteApplicationBuilderExtensions.cs => UmbracoApplicationBuilder.Website.cs} (52%) create mode 100644 src/Umbraco.Web.Website/Middleware/PublicAccessMiddleware.cs diff --git a/src/Umbraco.Core/Routing/IPublishedRouter.cs b/src/Umbraco.Core/Routing/IPublishedRouter.cs index c035a9b85b37..5de826d22269 100644 --- a/src/Umbraco.Core/Routing/IPublishedRouter.cs +++ b/src/Umbraco.Core/Routing/IPublishedRouter.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Umbraco.Cms.Core.Models.PublishedContent; namespace Umbraco.Cms.Core.Routing { @@ -24,18 +25,28 @@ public interface IPublishedRouter Task RouteRequestAsync(IPublishedRequestBuilder request, RouteRequestOptions options); /// - /// Updates the request to "not found". + /// Updates the request to use the specified item, or NULL /// /// The request. /// - /// A new based on values from the original - /// This method is invoked when the pipeline decides it cannot render + /// + /// A new based on values from the original + /// and with the re-routed values based on the passed in + /// + /// + /// This method is used for 2 cases: + /// - When the rendering content needs to change due to Public Access rules. + /// - When there is nothing to render due to circumstances such as no template files. In this case, NULL is used as the parameter. + /// + /// + /// This method is invoked when the pipeline decides it cannot render /// the request, for whatever reason, and wants to force it to be re-routed - /// and rendered as if no document were found (404). - /// This occurs if there is no template found and route hijacking was not matched. + /// and rendered as if no document were found (404). + /// This occurs if there is no template found and route hijacking was not matched. /// In that case it's the same as if there was no content which means even if there was - /// content matched we want to run the request through the last chance finders. + /// content matched we want to run the request through the last chance finders. + /// /// - Task UpdateRequestToNotFoundAsync(IPublishedRequest request); + Task UpdateRequestAsync(IPublishedRequest request, IPublishedContent publishedContent); } } diff --git a/src/Umbraco.Core/Routing/PublishedRouter.cs b/src/Umbraco.Core/Routing/PublishedRouter.cs index 6b66862f352a..f8697e640abd 100644 --- a/src/Umbraco.Core/Routing/PublishedRouter.cs +++ b/src/Umbraco.Core/Routing/PublishedRouter.cs @@ -11,7 +11,6 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; @@ -33,10 +32,8 @@ public class PublishedRouter : IPublishedRouter private readonly IPublishedUrlProvider _publishedUrlProvider; private readonly IRequestAccessor _requestAccessor; private readonly IPublishedValueFallback _publishedValueFallback; - private readonly IPublicAccessChecker _publicAccessChecker; private readonly IFileService _fileService; private readonly IContentTypeService _contentTypeService; - private readonly IPublicAccessService _publicAccessService; private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly IEventAggregator _eventAggregator; @@ -53,10 +50,8 @@ public PublishedRouter( IPublishedUrlProvider publishedUrlProvider, IRequestAccessor requestAccessor, IPublishedValueFallback publishedValueFallback, - IPublicAccessChecker publicAccessChecker, IFileService fileService, IContentTypeService contentTypeService, - IPublicAccessService publicAccessService, IUmbracoContextAccessor umbracoContextAccessor, IEventAggregator eventAggregator) { @@ -69,10 +64,8 @@ public PublishedRouter( _publishedUrlProvider = publishedUrlProvider; _requestAccessor = requestAccessor; _publishedValueFallback = publishedValueFallback; - _publicAccessChecker = publicAccessChecker; _fileService = fileService; _contentTypeService = contentTypeService; - _publicAccessService = publicAccessService; _umbracoContextAccessor = umbracoContextAccessor; _eventAggregator = eventAggregator; } @@ -104,13 +97,11 @@ private IPublishedRequest TryRouteRequest(IPublishedRequestBuilder request) { FindDomain(request); - // TODO: This was ported from v8 but how could it possibly have a redirect here? if (request.IsRedirect()) { return request.Build(); } - // TODO: This was ported from v8 but how could it possibly have content here? if (request.HasPublishedContent()) { return request.Build(); @@ -133,54 +124,78 @@ private void SetVariationContext(string culture) } /// - public async Task RouteRequestAsync(IPublishedRequestBuilder request, RouteRequestOptions options) + public async Task RouteRequestAsync(IPublishedRequestBuilder builder, RouteRequestOptions options) { // outbound routing performs different/simpler logic if (options.RouteDirection == RouteDirection.Outbound) { - return TryRouteRequest(request); + return TryRouteRequest(builder); } // find domain - FindDomain(request); + if (builder.Domain == null) + { + FindDomain(builder); + } + + await RouteRequestInternalAsync(builder); + + // complete the PCR and assign the remaining values + return BuildRequest(builder); + } - // TODO: This was ported from v8 but how could it possibly have a redirect here? - // if request has been flagged to redirect then return + private async Task RouteRequestInternalAsync(IPublishedRequestBuilder builder) + { + // if request builder was already flagged to redirect then return // whoever called us is in charge of actually redirecting - if (request.IsRedirect()) + if (builder.IsRedirect()) { - return request.Build(); + return; } // set the culture - SetVariationContext(request.Culture); + SetVariationContext(builder.Culture); + + var foundContentByFinders = false; - // find the published content if it's not assigned. This could be manually assigned with a custom route handler, or - // with something like EnsurePublishedContentRequestAttribute or UmbracoVirtualNodeRouteHandler. Those in turn call this method + // Find the published content if it's not assigned. + // This could be manually assigned with a custom route handler, etc... + // which in turn could call this method // to setup the rest of the pipeline but we don't want to run the finders since there's one assigned. - // TODO: This might very well change when we look into porting custom routes in netcore like EnsurePublishedContentRequestAttribute or UmbracoVirtualNodeRouteHandler. - if (!request.HasPublishedContent()) + if (!builder.HasPublishedContent()) { - // find the document & template - FindPublishedContentAndTemplate(request); + _logger.LogDebug("FindPublishedContentAndTemplate: Path={UriAbsolutePath}", builder.Uri.AbsolutePath); + + // run the document finders + foundContentByFinders = FindPublishedContent(builder); } - // handle wildcard domains - HandleWildcardDomains(request); + // if we are not a redirect + if (!builder.IsRedirect()) + { + // handle not-found, redirects, access... + HandlePublishedContent(builder); + + // find a template + FindTemplate(builder, foundContentByFinders); + + // handle umbracoRedirect + FollowExternalRedirect(builder); - // set the culture -- again, 'cos it might have changed due to a finder or wildcard domain - SetVariationContext(request.Culture); + // handle wildcard domains + HandleWildcardDomains(builder); + + // set the culture -- again, 'cos it might have changed due to a finder or wildcard domain + SetVariationContext(builder.Culture); + } // trigger the routing request (used to be called Prepared) event - at that point it is still possible to change about anything // even though the request might be flagged for redirection - we'll redirect _after_ the event - var routingRequest = new RoutingRequestNotification(request); + var routingRequest = new RoutingRequestNotification(builder); await _eventAggregator.PublishAsync(routingRequest); // we don't take care of anything so if the content has changed, it's up to the user // to find out the appropriate template - - // complete the PCR and assign the remaining values - return BuildRequest(request); } /// @@ -193,11 +208,11 @@ public async Task RouteRequestAsync(IPublishedRequestBuilder /// This method logic has been put into it's own method in case developers have created a custom PCR or are assigning their own values /// but need to finalize it themselves. /// - internal IPublishedRequest BuildRequest(IPublishedRequestBuilder frequest) + internal IPublishedRequest BuildRequest(IPublishedRequestBuilder builder) { - IPublishedRequest result = frequest.Build(); + IPublishedRequest result = builder.Build(); - if (!frequest.HasPublishedContent()) + if (!builder.HasPublishedContent()) { return result; } @@ -209,23 +224,26 @@ internal IPublishedRequest BuildRequest(IPublishedRequestBuilder frequest) } /// - public async Task UpdateRequestToNotFoundAsync(IPublishedRequest request) + public async Task UpdateRequestAsync(IPublishedRequest request, IPublishedContent publishedContent) { - var builder = new PublishedRequestBuilder(request.Uri, _fileService); - - // clear content + // store the original (if any) IPublishedContent content = request.PublishedContent; - builder.SetPublishedContent(null); - await HandlePublishedContentAsync(builder); // will go 404 - FindTemplate(builder, false); + IPublishedRequestBuilder builder = new PublishedRequestBuilder(request.Uri, _fileService); - // if request has been flagged to redirect then return - if (request.IsRedirect()) + // set to the new content (or null if specified) + builder.SetPublishedContent(publishedContent); + + // re-route + await RouteRequestInternalAsync(builder); + + // return if we are redirect + if (builder.IsRedirect()) { - return builder; + return BuildRequest(builder); } + // this will occur if publishedContent is null and the last chance finders also don't assign content if (!builder.HasPublishedContent()) { // means the engine could not find a proper document to handle 404 @@ -233,7 +251,7 @@ public async Task UpdateRequestToNotFoundAsync(IPublis builder.SetPublishedContent(content); } - return builder; + return BuildRequest(builder); } /// @@ -359,44 +377,11 @@ internal bool FindTemplateRenderingEngineInDirectory(DirectoryInfo directory, st return directory.GetFiles().Any(f => extensions.Any(e => f.Name.InvariantEquals(alias + e))); } - /// - /// Finds the Umbraco document (if any) matching the request, and updates the PublishedRequest accordingly. - /// - private void FindPublishedContentAndTemplate(IPublishedRequestBuilder request) - { - _logger.LogDebug("FindPublishedContentAndTemplate: Path={UriAbsolutePath}", request.Uri.AbsolutePath); - - // run the document finders - FindPublishedContent(request); - - // if request has been flagged to redirect then return - // whoever called us is in charge of actually redirecting - // -- do not process anything any further -- - if (request.IsRedirect()) - { - return; - } - - var foundContentByFinders = request.HasPublishedContent(); - - // not handling umbracoRedirect here but after LookupDocument2 - // so internal redirect, 404, etc has precedence over redirect - - // handle not-found, redirects, access... - HandlePublishedContentAsync(request); - - // find a template - FindTemplate(request, foundContentByFinders); - - // handle umbracoRedirect - FollowExternalRedirect(request); - } - /// /// Tries to find the document matching the request, by running the IPublishedContentFinder instances. /// /// There is no finder collection. - internal void FindPublishedContent(IPublishedRequestBuilder request) + internal bool FindPublishedContent(IPublishedRequestBuilder request) { const string tracePrefix = "FindPublishedContent: "; @@ -422,6 +407,8 @@ internal void FindPublishedContent(IPublishedRequestBuilder request) request.HasDomain() ? request.Domain.ToString() : "NULL", request.Culture ?? "NULL", request.ResponseStatusCode); + + return found; } } @@ -430,10 +417,10 @@ internal void FindPublishedContent(IPublishedRequestBuilder request) /// /// The request builder. /// - /// Handles "not found", internal redirects, access validation... + /// Handles "not found", internal redirects ... /// things that must be handled in one place because they can create loops /// - private async Task HandlePublishedContentAsync(IPublishedRequestBuilder request) + private void HandlePublishedContent(IPublishedRequestBuilder request) { // because these might loop, we have to have some sort of infinite loop detection int i = 0, j = 0; @@ -469,12 +456,6 @@ private async Task HandlePublishedContentAsync(IPublishedRequestBuilder request) break; } - // ensure access - if (request.PublishedContent != null) - { - await EnsurePublishedContentAccess(request); - } - // loop while we don't have page, ie the redirect or access // got us to nowhere and now we need to run the notFoundLookup again // as long as it's not running out of control ie infinite loop of some sort @@ -573,63 +554,6 @@ private bool FollowInternalRedirects(IPublishedRequestBuilder request) return redirect; } - /// - /// Ensures that access to current node is permitted. - /// - /// Redirecting to a different site root and/or culture will not pick the new site root nor the new culture. - private async Task EnsurePublishedContentAccess(IPublishedRequestBuilder request) - { - if (request.PublishedContent == null) - { - throw new InvalidOperationException("There is no PublishedContent."); - } - - var path = request.PublishedContent.Path; - - Attempt publicAccessAttempt = _publicAccessService.IsProtected(path); - - if (publicAccessAttempt) - { - _logger.LogDebug("EnsurePublishedContentAccess: Page is protected, check for access"); - - PublicAccessStatus status = await _publicAccessChecker.HasMemberAccessToContentAsync(request.PublishedContent.Id); - switch (status) - { - case PublicAccessStatus.NotLoggedIn: - _logger.LogDebug("EnsurePublishedContentAccess: Not logged in, redirect to login page"); - SetPublishedContentAsOtherPage(request, publicAccessAttempt.Result.LoginNodeId); - break; - case PublicAccessStatus.AccessDenied: - _logger.LogDebug("EnsurePublishedContentAccess: Current member has not access, redirect to error page"); - SetPublishedContentAsOtherPage(request, publicAccessAttempt.Result.NoAccessNodeId); - break; - case PublicAccessStatus.LockedOut: - _logger.LogDebug("Current member is locked out, redirect to error page"); - SetPublishedContentAsOtherPage(request, publicAccessAttempt.Result.NoAccessNodeId); - break; - case PublicAccessStatus.NotApproved: - _logger.LogDebug("Current member is unapproved, redirect to error page"); - SetPublishedContentAsOtherPage(request, publicAccessAttempt.Result.NoAccessNodeId); - break; - case PublicAccessStatus.AccessAccepted: - _logger.LogDebug("Current member has access"); - break; - } - } - else - { - _logger.LogDebug("EnsurePublishedContentAccess: Page is not protected"); - } - } - - private void SetPublishedContentAsOtherPage(IPublishedRequestBuilder request, int errorPageId) - { - if (errorPageId != request.PublishedContent.Id) - { - request.SetPublishedContent(_umbracoContextAccessor.UmbracoContext.PublishedSnapshot.Content.GetById(errorPageId)); - } - } - /// /// Finds a template for the current node, if any. /// diff --git a/src/Umbraco.Infrastructure/Security/IMemberManager.cs b/src/Umbraco.Infrastructure/Security/IMemberManager.cs index e185c2a0ae9c..1fc035d87620 100644 --- a/src/Umbraco.Infrastructure/Security/IMemberManager.cs +++ b/src/Umbraco.Infrastructure/Security/IMemberManager.cs @@ -8,6 +8,12 @@ namespace Umbraco.Cms.Core.Security /// public interface IMemberManager : IUmbracoUserManager { + /// + /// Returns the currently logged in member if there is one, else returns null + /// + /// + Task GetCurrentMemberAsync(); + /// /// Checks if the current member is authorized based on the parameters provided. /// diff --git a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index d6492f67bbed..a264b74ffcc7 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -181,8 +181,8 @@ public override void ConfigureServices(IServiceCollection services) public override void Configure(IApplicationBuilder app) { app.UseUmbraco(); - app.UseUmbracoBackOffice(); - app.UseUmbracoWebsite(); + app.UseUmbracoBackOfficeEndpoints(); + app.UseUmbracoWebsiteEndpoints(); } } } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactoryTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactoryTests.cs index dc8ec7b6ff66..2bd482b8eb49 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactoryTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactoryTests.cs @@ -34,11 +34,11 @@ private UmbracoRouteValuesFactory GetFactory( { var builder = new PublishedRequestBuilder(new Uri("https://example.com"), Mock.Of()); builder.SetPublishedContent(Mock.Of()); - request = builder.Build(); + IPublishedRequest builtRequest = request = builder.Build(); publishedRouter = new Mock(); - publishedRouter.Setup(x => x.UpdateRequestToNotFoundAsync(It.IsAny())) - .Returns((IPublishedRequest r) => Task.FromResult((IPublishedRequestBuilder)builder)) + publishedRouter.Setup(x => x.UpdateRequestAsync(It.IsAny(), null)) + .Returns((IPublishedRequest r, IPublishedContent c) => Task.FromResult(builtRequest)) .Verifiable(); renderingDefaults = new UmbracoRenderingDefaults(); @@ -76,7 +76,7 @@ public async Task Update_Request_To_Not_Found_When_No_Template() UmbracoRouteValues result = await factory.CreateAsync(new DefaultHttpContext(), request); // The request has content, no template, no hijacked route and no disabled template features so UpdateRequestToNotFound will be called - publishedRouter.Verify(m => m.UpdateRequestToNotFoundAsync(It.IsAny()), Times.Once); + publishedRouter.Verify(m => m.UpdateRequestAsync(It.IsAny(), null), Times.Once); } [Test] diff --git a/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs b/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs index 2c8465c1ea34..b44683da5a73 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs @@ -110,10 +110,8 @@ internal static PublishedRouter CreatePublishedRouter(IUmbracoContextAccessor um Mock.Of(), Mock.Of(), container?.GetRequiredService() ?? Current.Factory.GetRequiredService(), - container?.GetRequiredService() ?? Current.Factory.GetRequiredService(), container?.GetRequiredService() ?? Current.Factory.GetRequiredService(), container?.GetRequiredService() ?? Current.Factory.GetRequiredService(), - container?.GetRequiredService() ?? Current.Factory.GetRequiredService(), umbracoContextAccessor, Mock.Of() ); diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index 7ce9a83a3e10..0e75cf90deb1 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -83,7 +83,10 @@ public static IUmbracoBuilder AddBackOfficeAuthentication(this IUmbracoBuilder b builder.Services.ConfigureOptions(); - builder.Services.AddUnique(); + builder.Services + .AddSingleton() + .ConfigureOptions(); + builder.Services.AddUnique(); builder.Services.AddUnique, PasswordChanger>(); builder.Services.AddUnique, PasswordChanger>(); diff --git a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs deleted file mode 100644 index 813d5cd096b7..000000000000 --- a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Web.BackOffice.Middleware; -using Umbraco.Cms.Web.BackOffice.Routing; -using Umbraco.Cms.Web.BackOffice.Security; - -namespace Umbraco.Extensions -{ - /// - /// extensions for Umbraco - /// - public static class BackOfficeApplicationBuilderExtensions - { - public static IApplicationBuilder UseUmbracoBackOffice(this IApplicationBuilder app) - { - // NOTE: This method will have been called after UseRouting, UseAuthentication, UseAuthorization - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - if (!app.UmbracoCanBoot()) - { - return app; - } - - app.UseEndpoints(endpoints => - { - BackOfficeAreaRoutes backOfficeRoutes = app.ApplicationServices.GetRequiredService(); - backOfficeRoutes.CreateRoutes(endpoints); - }); - - app.UseUmbracoRuntimeMinification(); - - app.UseMiddleware(); - - app.UseUmbracoPreview(); - - return app; - } - - public static IApplicationBuilder UseUmbracoPreview(this IApplicationBuilder app) - { - app.UseEndpoints(endpoints => - { - PreviewRoutes previewRoutes = app.ApplicationServices.GetRequiredService(); - previewRoutes.CreateRoutes(endpoints); - }); - - return app; - } - } -} diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs new file mode 100644 index 000000000000..0f5ee616e315 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs @@ -0,0 +1,40 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Web.BackOffice.Routing; +using Umbraco.Cms.Web.Common.ApplicationBuilder; +using Umbraco.Cms.Web.Common.Extensions; + +namespace Umbraco.Extensions +{ + /// + /// extensions for Umbraco + /// + public static partial class UmbracoApplicationBuilderExtensions + { + public static IUmbracoApplicationBuilder UseBackOfficeEndpoints(this IUmbracoApplicationBuilder app) + { + // NOTE: This method will have been called after UseRouting, UseAuthentication, UseAuthorization + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (!app.AppBuilder.UmbracoCanBoot()) + { + return app; + } + + app.AppBuilder.UseEndpoints(endpoints => + { + BackOfficeAreaRoutes backOfficeRoutes = app.AppBuilder.ApplicationServices.GetRequiredService(); + backOfficeRoutes.CreateRoutes(endpoints); + }); + + app.UseUmbracoRuntimeMinificationEndpoints(); + app.UseUmbracoPreviewEndpoints(); + + return app; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.Preview.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.Preview.cs new file mode 100644 index 000000000000..fadfbaf78f80 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.Preview.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Web.BackOffice.Routing; +using Umbraco.Cms.Web.Common.ApplicationBuilder; + +namespace Umbraco.Extensions +{ + /// + /// extensions for Umbraco + /// + public static partial class UmbracoApplicationBuilderExtensions + { + public static IUmbracoApplicationBuilder UseUmbracoPreviewEndpoints(this IUmbracoApplicationBuilder app) + { + app.AppBuilder.UseEndpoints(endpoints => + { + PreviewRoutes previewRoutes = app.AppBuilder.ApplicationServices.GetRequiredService(); + previewRoutes.CreateRoutes(endpoints); + }); + + return app; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Middleware/BackOfficeExternalLoginProviderErrorMiddleware.cs b/src/Umbraco.Web.BackOffice/Middleware/BackOfficeExternalLoginProviderErrorMiddleware.cs index 796443bbf6c7..9838660663a3 100644 --- a/src/Umbraco.Web.BackOffice/Middleware/BackOfficeExternalLoginProviderErrorMiddleware.cs +++ b/src/Umbraco.Web.BackOffice/Middleware/BackOfficeExternalLoginProviderErrorMiddleware.cs @@ -1,9 +1,12 @@ -using System; +using System; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.Extensions.Options; using Newtonsoft.Json; +using Umbraco.Cms.Web.Common.ApplicationBuilder; using Umbraco.Extensions; using HttpRequestExtensions = Umbraco.Extensions.HttpRequestExtensions; @@ -16,8 +19,19 @@ namespace Umbraco.Cms.Web.BackOffice.Middleware /// When an external login provider registers an error with during the OAuth process, /// this middleware will detect that, store the errors into cookie data and redirect to the back office login so we can read the errors back out. /// - public class BackOfficeExternalLoginProviderErrorMiddleware : IMiddleware + public class BackOfficeExternalLoginProviderErrorMiddleware : IMiddleware, IConfigureOptions { + /// + /// Adds ourselves to the Umbraco middleware pipeline before any endpoint routes are declared + /// + /// + public void Configure(UmbracoPipelineOptions options) + => options.AddFilter(new UmbracoPipelineFilter( + nameof(BackOfficeExternalLoginProviderErrorMiddleware), + null, + app => app.UseMiddleware(), + null)); + public async Task InvokeAsync(HttpContext context, RequestDelegate next) { var shortCircuit = false; diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilder.cs b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilder.cs new file mode 100644 index 000000000000..20240a569efa --- /dev/null +++ b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilder.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; + +namespace Umbraco.Cms.Web.Common.ApplicationBuilder +{ + /// + /// A builder to allow encapsulating the enabled routing features in Umbraco + /// + public interface IUmbracoApplicationBuilder + { + /// + /// Returns the + /// + IApplicationBuilder AppBuilder { get; } + } +} diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoPipelineFilter.cs b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoPipelineFilter.cs new file mode 100644 index 000000000000..d1cf866da12d --- /dev/null +++ b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoPipelineFilter.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Builder; + +namespace Umbraco.Cms.Web.Common.ApplicationBuilder +{ + /// + /// Used to modify the pipeline before and after Umbraco registers it's core middlewares. + /// + /// + /// Mainly used for package developers. + /// + public interface IUmbracoPipelineFilter + { + /// + /// The name of the filter + /// + /// + /// This can be used by developers to see what is registered and if anything should be re-ordered, removed, etc... + /// + string Name { get; } + + /// + /// Executes before Umbraco middlewares are registered + /// + /// + void OnPrePipeline(IApplicationBuilder app); + + /// + /// Executes after core Umbraco middlewares are registered and before any Endpoints are declared + /// + /// + void OnPostPipeline(IApplicationBuilder app); + + /// + /// Executes after just before any Umbraco endpoints are declared. + /// + /// + void OnEndpoints(IApplicationBuilder app); + } +} diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs new file mode 100644 index 000000000000..b8a543178600 --- /dev/null +++ b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Builder; + +namespace Umbraco.Cms.Web.Common.ApplicationBuilder +{ + /// + /// A builder to allow encapsulating the enabled routing features in Umbraco + /// + internal class UmbracoApplicationBuilder : IUmbracoApplicationBuilder + { + public UmbracoApplicationBuilder(IApplicationBuilder appBuilder) + { + AppBuilder = appBuilder; + } + + public IApplicationBuilder AppBuilder { get; } + } +} diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoPipelineFilter.cs b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoPipelineFilter.cs new file mode 100644 index 000000000000..625cfc41d88b --- /dev/null +++ b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoPipelineFilter.cs @@ -0,0 +1,37 @@ +using System; +using Microsoft.AspNetCore.Builder; + +namespace Umbraco.Cms.Web.Common.ApplicationBuilder +{ + /// + /// Used to modify the pipeline before and after Umbraco registers it's core middlewares. + /// + /// + /// Mainly used for package developers. + /// + public class UmbracoPipelineFilter : IUmbracoPipelineFilter + { + public UmbracoPipelineFilter(string name) : this(name, null, null, null) { } + + public UmbracoPipelineFilter( + string name, + Action prePipeline, + Action postPipeline, + Action endpointCallback) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + PrePipeline = prePipeline; + PostPipeline = postPipeline; + Endpoints = endpointCallback; + } + + public Action PrePipeline { get; set; } + public Action PostPipeline { get; set; } + public Action Endpoints { get; set; } + public string Name { get; } + + public void OnPrePipeline(IApplicationBuilder app) => PrePipeline?.Invoke(app); + public void OnPostPipeline(IApplicationBuilder app) => PostPipeline?.Invoke(app); + public void OnEndpoints(IApplicationBuilder app) => Endpoints?.Invoke(app); + } +} diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoPipelineOptions.cs b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoPipelineOptions.cs new file mode 100644 index 000000000000..8cf2b4144ab1 --- /dev/null +++ b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoPipelineOptions.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Builder; + +namespace Umbraco.Cms.Web.Common.ApplicationBuilder +{ + /// + /// Options to allow modifying the pipeline before and after Umbraco registers it's core middlewares. + /// + public class UmbracoPipelineOptions + { + /// + /// Returns a mutable list of all registered startup filters + /// + public IList PipelineFilters { get; } = new List(); + + /// + /// Adds a filter to the list + /// + /// + public void AddFilter(IUmbracoPipelineFilter filter) => PipelineFilters.Add(filter); + } +} diff --git a/src/Umbraco.Web.Common/Controllers/RenderController.cs b/src/Umbraco.Web.Common/Controllers/RenderController.cs index 12896b79986a..c6bedf7e6eb2 100644 --- a/src/Umbraco.Web.Common/Controllers/RenderController.cs +++ b/src/Umbraco.Web.Common/Controllers/RenderController.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Web; @@ -113,6 +114,6 @@ private PublishedContentNotFoundResult GetNoTemplateResult(IPublishedRequest pcr { return new PublishedContentNotFoundResult(UmbracoContext); } - } + } } } diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoApplicationServicesCapture.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoApplicationServicesCapture.cs new file mode 100644 index 000000000000..fa5adf7aeb0e --- /dev/null +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoApplicationServicesCapture.cs @@ -0,0 +1,20 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; + +namespace Umbraco.Cms.Web.Common.DependencyInjection +{ + /// + /// A registered to automatically capture application services + /// + internal class UmbracoApplicationServicesCapture : IStartupFilter + { + /// + public Action Configure(Action next) => + app => + { + StaticServiceProvider.Instance = app.ApplicationServices; + next(app); + }; + } +} diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index d34f7f373082..fb748403954f 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -112,7 +112,7 @@ public static IUmbracoBuilder AddUmbraco( // adds the umbraco startup filter which will call UseUmbraco early on before // other start filters are applied (depending on the ordering of IStartupFilters in DI). - services.AddTransient(); + services.AddTransient(); return new UmbracoBuilder(services, config, typeLoader, loggerFactory); } diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoStartupFilter.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoStartupFilter.cs deleted file mode 100644 index f8ca73e283c8..000000000000 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoStartupFilter.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Options; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Web.Common.DependencyInjection -{ - /// - /// A registered early in DI so that it executes before any user IStartupFilters - /// to ensure that all Umbraco service and requirements are started correctly and in order. - /// - public sealed class UmbracoStartupFilter : IStartupFilter - { - private readonly IOptions _options; - public UmbracoStartupFilter(IOptions options) => _options = options; - - /// - public Action Configure(Action next) => - app => - { - StaticServiceProvider.Instance = app.ApplicationServices; - _options.Value.PreUmbracoPipeline(app); - - app.UseUmbraco(); - next(app); - }; - } -} diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoStartupFilterOptions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoStartupFilterOptions.cs deleted file mode 100644 index 46ed09006b79..000000000000 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoStartupFilterOptions.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace Umbraco.Cms.Web.Common.DependencyInjection -{ - public class UmbracoStartupFilterOptions - { - /// - /// Represents the pipeline that is executed before umbraco. By default this pipeline only adds UseDeveloperExceptionPage when the environments is Development. - /// - public Action PreUmbracoPipeline { get; set; } = app => - { - IWebHostEnvironment env = app.ApplicationServices.GetRequiredService(); - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - }; - } -} diff --git a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs index 1036a1f63026..8eddfd644140 100644 --- a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs @@ -5,14 +5,13 @@ using Microsoft.Extensions.Options; using Serilog.Context; using SixLabors.ImageSharp.Web.DependencyInjection; -using Smidge; -using Smidge.Nuglify; using StackExchange.Profiling; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Logging.Serilog.Enrichers; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.ApplicationBuilder; using Umbraco.Cms.Web.Common.Middleware; using Umbraco.Cms.Web.Common.Plugins; @@ -26,8 +25,12 @@ public static class ApplicationBuilderExtensions /// /// Configures and use services required for using Umbraco /// - public static IApplicationBuilder UseUmbraco(this IApplicationBuilder app) + public static IApplicationBuilder UseUmbraco(this IApplicationBuilder app, Action configureUmbraco) { + IOptions startupOptions = app.ApplicationServices.GetRequiredService>(); + + app.RunPrePipeline(startupOptions.Value); + // TODO: Should we do some checks like this to verify that the corresponding "Add" methods have been called for the // corresponding "Use" methods? // https://github.com/dotnet/aspnetcore/blob/b795ac3546eb3e2f47a01a64feb3020794ca33bb/src/Mvc/Mvc.Core/src/Builder/MvcApplicationBuilderExtensions.cs#L132 @@ -66,12 +69,45 @@ public static IApplicationBuilder UseUmbraco(this IApplicationBuilder app) // Must be called after UseRouting and before UseEndpoints app.UseSession(); - // Must come after the above! - app.UseUmbracoInstaller(); + // DO NOT PUT ANY UseEndpoints declarations here!! Those must all come very last in the pipeline, + // endpoints are terminating middleware. + + app.RunPostPipeline(startupOptions.Value); + app.RunPreEndpointsPipeline(startupOptions.Value); + + // create our custom builder and execute the callback + // which will allow executing all IUmbracoApplicationBuilder ext methods + // to create endpoints. + var umbAppBuilder = new UmbracoApplicationBuilder(app); + configureUmbraco(umbAppBuilder); return app; } + private static void RunPrePipeline(this IApplicationBuilder app, UmbracoPipelineOptions startupOptions) + { + foreach (IUmbracoPipelineFilter filter in startupOptions.PipelineFilters) + { + filter.OnPrePipeline(app); + } + } + + private static void RunPostPipeline(this IApplicationBuilder app, UmbracoPipelineOptions startupOptions) + { + foreach (IUmbracoPipelineFilter filter in startupOptions.PipelineFilters) + { + filter.OnPostPipeline(app); + } + } + + private static void RunPreEndpointsPipeline(this IApplicationBuilder app, UmbracoPipelineOptions startupOptions) + { + foreach (IUmbracoPipelineFilter filter in startupOptions.PipelineFilters) + { + filter.OnEndpoints(app); + } + } + /// /// Returns true if Umbraco is greater than /// @@ -151,27 +187,6 @@ public static IApplicationBuilder UseUmbracoRequestLogging(this IApplicationBuil return app; } - /// - /// Enables runtime minification for Umbraco - /// - public static IApplicationBuilder UseUmbracoRuntimeMinification(this IApplicationBuilder app) - { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - if (!app.UmbracoCanBoot()) - { - return app; - } - - app.UseSmidge(); - app.UseSmidgeNuglify(); - - return app; - } - public static IApplicationBuilder UseUmbracoPlugins(this IApplicationBuilder app) { var hostingEnvironment = app.ApplicationServices.GetRequiredService(); diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoInstallApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.Installer.cs similarity index 54% rename from src/Umbraco.Web.Common/Extensions/UmbracoInstallApplicationBuilderExtensions.cs rename to src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.Installer.cs index 40c5c6364223..3adf7a9995c7 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoInstallApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.Installer.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Web.Common.ApplicationBuilder; using Umbraco.Cms.Web.Common.Install; namespace Umbraco.Extensions @@ -7,21 +8,21 @@ namespace Umbraco.Extensions /// /// extensions for Umbraco installer /// - public static class UmbracoInstallApplicationBuilderExtensions + public static partial class UmbracoApplicationBuilderExtensions { /// /// Enables the Umbraco installer /// - public static IApplicationBuilder UseUmbracoInstaller(this IApplicationBuilder app) + public static IUmbracoApplicationBuilder UseInstallerEndpoints(this IUmbracoApplicationBuilder app) { - if (!app.UmbracoCanBoot()) + if (!app.AppBuilder.UmbracoCanBoot()) { return app; } - app.UseEndpoints(endpoints => + app.AppBuilder.UseEndpoints(endpoints => { - InstallAreaRoutes installerRoutes = app.ApplicationServices.GetRequiredService(); + InstallAreaRoutes installerRoutes = app.AppBuilder.ApplicationServices.GetRequiredService(); installerRoutes.CreateRoutes(endpoints); }); diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.RuntimeMinification.cs b/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.RuntimeMinification.cs new file mode 100644 index 000000000000..0d962dd5a588 --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.RuntimeMinification.cs @@ -0,0 +1,32 @@ +using System; +using Smidge; +using Smidge.Nuglify; +using Umbraco.Cms.Web.Common.ApplicationBuilder; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Web.Common.Extensions +{ + public static partial class UmbracoApplicationBuilderExtensions + { + /// + /// Enables runtime minification for Umbraco + /// + public static IUmbracoApplicationBuilder UseUmbracoRuntimeMinificationEndpoints(this IUmbracoApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (!app.AppBuilder.UmbracoCanBoot()) + { + return app; + } + + app.AppBuilder.UseSmidge(); + app.AppBuilder.UseSmidgeNuglify(); + + return app; + } + } +} diff --git a/src/Umbraco.Web.Common/Security/MemberManager.cs b/src/Umbraco.Web.Common/Security/MemberManager.cs index 5a5b1414b547..873f335ec144 100644 --- a/src/Umbraco.Web.Common/Security/MemberManager.cs +++ b/src/Umbraco.Web.Common/Security/MemberManager.cs @@ -163,6 +163,20 @@ public Task> IsProtectedAsync(IEnumerable)result); } + /// + public async Task GetCurrentMemberAsync() + { + if (_currentMember == null) + { + if (!IsLoggedIn()) + { + return null; + } + _currentMember = await GetUserAsync(_httpContextAccessor.HttpContext.User); + } + return _currentMember; + } + /// /// This will check if the member has access to this path /// @@ -172,7 +186,7 @@ public Task> IsProtectedAsync(IEnumerable HasAccessAsync(string path) { MemberIdentityUser currentMember = await GetCurrentMemberAsync(); - if (currentMember == null) + if (currentMember == null || !currentMember.IsApproved || currentMember.IsLockedOut) { return false; } @@ -188,7 +202,7 @@ private async Task> HasAccessAsync(IEnumerable var result = new Dictionary(); MemberIdentityUser currentMember = await GetCurrentMemberAsync(); - if (currentMember == null) + if (currentMember == null || !currentMember.IsApproved || currentMember.IsLockedOut) { return result; } @@ -214,19 +228,6 @@ async Task> getUserRolesAsync() async () => await getUserRolesAsync()); } return result; - } - - private async Task GetCurrentMemberAsync() - { - if (_currentMember == null) - { - if (!IsLoggedIn()) - { - return null; - } - _currentMember = await GetUserAsync(_httpContextAccessor.HttpContext.User); - } - return _currentMember; - } + } } } diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index 820ec6d7065c..3eb29209a38c 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -4,7 +4,10 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Infrastructure.DependencyInjection; +using Umbraco.Cms.Web.BackOffice.ModelsBuilder; using Umbraco.Extensions; +using Microsoft.Extensions.Hosting; namespace Umbraco.Cms.Web.UI.NetCore { @@ -51,10 +54,19 @@ public void ConfigureServices(IServiceCollection services) /// /// Configures the application /// - public void Configure(IApplicationBuilder app) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { - app.UseUmbracoBackOffice(); - app.UseUmbracoWebsite(); + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseUmbraco(u => + { + u.UseInstallerEndpoints(); + u.UseBackOfficeEndpoints(); + u.UseWebsiteEndpoints(); + }); } } } diff --git a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs index 1bdad575c006..cebc164c041a 100644 --- a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs @@ -2,12 +2,11 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Core.Security; using Umbraco.Cms.Infrastructure.DependencyInjection; using Umbraco.Cms.Web.Common.Routing; -using Umbraco.Cms.Web.Common.Security; using Umbraco.Cms.Web.Website.Collections; using Umbraco.Cms.Web.Website.Controllers; +using Umbraco.Cms.Web.Website.Middleware; using Umbraco.Cms.Web.Website.Models; using Umbraco.Cms.Web.Website.Routing; using Umbraco.Cms.Web.Website.ViewEngines; @@ -48,12 +47,15 @@ public static IUmbracoBuilder AddWebsite(this IUmbracoBuilder builder) builder.Services.AddSingleton(); + builder.Services + .AddSingleton() + .ConfigureOptions(); + builder .AddDistributedCache() .AddModelsBuilder(); return builder; } - } } diff --git a/src/Umbraco.Web.Website/Extensions/UmbracoWebsiteApplicationBuilderExtensions.cs b/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs similarity index 52% rename from src/Umbraco.Web.Website/Extensions/UmbracoWebsiteApplicationBuilderExtensions.cs rename to src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs index 4f049abdacea..35d014f9157f 100644 --- a/src/Umbraco.Web.Website/Extensions/UmbracoWebsiteApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs @@ -1,6 +1,7 @@ using System; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Web.Common.ApplicationBuilder; using Umbraco.Cms.Web.Website.Routing; namespace Umbraco.Extensions @@ -8,36 +9,26 @@ namespace Umbraco.Extensions /// /// extensions for the umbraco front-end website /// - public static class UmbracoWebsiteApplicationBuilderExtensions + public static partial class UmbracoApplicationBuilderExtensions { /// - /// Sets up services and routes for the front-end umbraco website + /// Sets up routes for the front-end umbraco website /// - public static IApplicationBuilder UseUmbracoWebsite(this IApplicationBuilder app) + public static IUmbracoApplicationBuilder UseWebsiteEndpoints(this IUmbracoApplicationBuilder app) { if (app == null) { throw new ArgumentNullException(nameof(app)); } - if (!app.UmbracoCanBoot()) + if (!app.AppBuilder.UmbracoCanBoot()) { return app; } - app.UseUmbracoRoutes(); - - return app; - } - - /// - /// Sets up routes for the umbraco front-end - /// - public static IApplicationBuilder UseUmbracoRoutes(this IApplicationBuilder app) - { - app.UseEndpoints(endpoints => + app.AppBuilder.UseEndpoints(endpoints => { - FrontEndRoutes surfaceRoutes = app.ApplicationServices.GetRequiredService(); + FrontEndRoutes surfaceRoutes = app.AppBuilder.ApplicationServices.GetRequiredService(); surfaceRoutes.CreateRoutes(endpoints); endpoints.MapDynamicControllerRoute("/{**slug}"); diff --git a/src/Umbraco.Web.Website/Middleware/PublicAccessMiddleware.cs b/src/Umbraco.Web.Website/Middleware/PublicAccessMiddleware.cs new file mode 100644 index 000000000000..705e9a581f7d --- /dev/null +++ b/src/Umbraco.Web.Website/Middleware/PublicAccessMiddleware.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Web.Common.ApplicationBuilder; +using Umbraco.Cms.Web.Common.Routing; +using Umbraco.Cms.Web.Website.Routing; + +namespace Umbraco.Cms.Web.Website.Middleware +{ + public class PublicAccessMiddleware : IMiddleware, IConfigureOptions + { + private readonly ILogger _logger; + private readonly IPublicAccessService _publicAccessService; + private readonly IPublicAccessChecker _publicAccessChecker; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IUmbracoRouteValuesFactory _umbracoRouteValuesFactory; + private readonly IPublishedRouter _publishedRouter; + + public PublicAccessMiddleware( + ILogger logger, + IPublicAccessService publicAccessService, + IPublicAccessChecker publicAccessChecker, + IUmbracoContextAccessor umbracoContextAccessor, + IUmbracoRouteValuesFactory umbracoRouteValuesFactory, + IPublishedRouter publishedRouter) + { + _logger = logger; + _publicAccessService = publicAccessService; + _publicAccessChecker = publicAccessChecker; + _umbracoContextAccessor = umbracoContextAccessor; + _umbracoRouteValuesFactory = umbracoRouteValuesFactory; + _publishedRouter = publishedRouter; + } + + /// + /// Adds ourselves to the Umbraco middleware pipeline before any endpoint routes are declared + /// + /// + public void Configure(UmbracoPipelineOptions options) + => options.AddFilter(new UmbracoPipelineFilter( + nameof(PublicAccessMiddleware), + null, + app => app.UseMiddleware(), + null)); + + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + UmbracoRouteValues umbracoRouteValues = context.Features.Get(); + + if (umbracoRouteValues != null) + { + await EnsurePublishedContentAccess(context, umbracoRouteValues); + } + + await next(context); + } + + /// + /// Ensures that access to current node is permitted. + /// + /// Redirecting to a different site root and/or culture will not pick the new site root nor the new culture. + private async Task EnsurePublishedContentAccess(HttpContext httpContext, UmbracoRouteValues routeValues) + { + // because these might loop, we have to have some sort of infinite loop detection + int i = 0; + const int maxLoop = 8; + PublicAccessStatus publicAccessStatus = PublicAccessStatus.AccessAccepted; + do + { + _logger.LogDebug(nameof(EnsurePublishedContentAccess) + ": Loop {LoopCounter}", i); + + + IPublishedContent publishedContent = routeValues.PublishedRequest?.PublishedContent; + if (publishedContent == null) + { + throw new InvalidOperationException("There is no PublishedContent."); + } + + var path = publishedContent.Path; + + Attempt publicAccessAttempt = _publicAccessService.IsProtected(path); + + if (publicAccessAttempt) + { + _logger.LogDebug("EnsurePublishedContentAccess: Page is protected, check for access"); + + publicAccessStatus = await _publicAccessChecker.HasMemberAccessToContentAsync(publishedContent.Id); + switch (publicAccessStatus) + { + case PublicAccessStatus.NotLoggedIn: + _logger.LogDebug("EnsurePublishedContentAccess: Not logged in, redirect to login page"); + routeValues = await SetPublishedContentAsOtherPageAsync(httpContext, routeValues.PublishedRequest, publicAccessAttempt.Result.LoginNodeId); + break; + case PublicAccessStatus.AccessDenied: + _logger.LogDebug("EnsurePublishedContentAccess: Current member has not access, redirect to error page"); + routeValues = await SetPublishedContentAsOtherPageAsync(httpContext, routeValues.PublishedRequest, publicAccessAttempt.Result.NoAccessNodeId); + break; + case PublicAccessStatus.LockedOut: + _logger.LogDebug("Current member is locked out, redirect to error page"); + routeValues = await SetPublishedContentAsOtherPageAsync(httpContext, routeValues.PublishedRequest, publicAccessAttempt.Result.NoAccessNodeId); + break; + case PublicAccessStatus.NotApproved: + _logger.LogDebug("Current member is unapproved, redirect to error page"); + routeValues = await SetPublishedContentAsOtherPageAsync(httpContext, routeValues.PublishedRequest, publicAccessAttempt.Result.NoAccessNodeId); + break; + case PublicAccessStatus.AccessAccepted: + _logger.LogDebug("Current member has access"); + break; + } + } + else + { + publicAccessStatus = PublicAccessStatus.AccessAccepted; + _logger.LogDebug("EnsurePublishedContentAccess: Page is not protected"); + } + + + //loop until we have access or reached max loops + } while (routeValues != null && publicAccessStatus != PublicAccessStatus.AccessAccepted && i++ < maxLoop); + + if (i == maxLoop) + { + _logger.LogDebug(nameof(EnsurePublishedContentAccess) + ": Looks like we are running into an infinite loop, abort"); + } + } + + + + private async Task SetPublishedContentAsOtherPageAsync(HttpContext httpContext, IPublishedRequest publishedRequest, int pageId) + { + if (pageId != publishedRequest.PublishedContent.Id) + { + IPublishedContent publishedContent = _umbracoContextAccessor.UmbracoContext.PublishedSnapshot.Content.GetById(pageId); + if (publishedContent == null) + { + throw new InvalidOperationException("No content found by id " + pageId); + } + + IPublishedRequest reRouted = await _publishedRouter.UpdateRequestAsync(publishedRequest, publishedContent); + + // we need to change the content item that is getting rendered so we have to re-create UmbracoRouteValues. + UmbracoRouteValues updatedRouteValues = await _umbracoRouteValuesFactory.CreateAsync(httpContext, reRouted); + + // Update the feature + httpContext.Features.Set(updatedRouteValues); + + return updatedRouteValues; + } + else + { + _logger.LogWarning("Public Access rule has a redirect node set to itself, nothing can be routed."); + // Update the feature to nothing - cannot continue + httpContext.Features.Set(null); + return null; + } + } + } +} diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs index 4c42f67be589..eceae1146205 100644 --- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Net; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Controllers; @@ -114,7 +115,7 @@ public override async ValueTask TransformAsync(HttpContext return values; } - IPublishedRequest publishedRequest = await RouteRequestAsync(_umbracoContextAccessor.UmbracoContext); + IPublishedRequest publishedRequest = await RouteRequestAsync(httpContext, _umbracoContextAccessor.UmbracoContext); UmbracoRouteValues umbracoRouteValues = await _routeValuesFactory.CreateAsync(httpContext, publishedRequest); @@ -137,7 +138,7 @@ public override async ValueTask TransformAsync(HttpContext return values; } - private async Task RouteRequestAsync(IUmbracoContext umbracoContext) + private async Task RouteRequestAsync(HttpContext httpContext, IUmbracoContext umbracoContext) { // ok, process diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactory.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactory.cs index 0c220afb304a..e1b85919f485 100644 --- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactory.cs +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactory.cs @@ -148,15 +148,14 @@ private async Task CheckNoTemplateAsync(HttpContext httpCont // This is basically a 404 even if there is content found. // We then need to re-run this through the pipeline for the last // chance finders to work. - IPublishedRequestBuilder builder = await _publishedRouter.UpdateRequestToNotFoundAsync(request); + // Set to null since we are telling it there is no content. + request = await _publishedRouter.UpdateRequestAsync(request, null); - if (builder == null) + if (request == null) { - throw new InvalidOperationException($"The call to {nameof(IPublishedRouter.UpdateRequestToNotFoundAsync)} cannot return null"); + throw new InvalidOperationException($"The call to {nameof(IPublishedRouter.UpdateRequestAsync)} cannot return null"); } - request = builder.Build(); - def = new UmbracoRouteValues( request, def.ControllerActionDescriptor, From 07e1ffee299fd9ca785291187187482bce3fbd27 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 15 Apr 2021 21:33:11 +1000 Subject: [PATCH 07/26] adds note --- src/Umbraco.Core/PublishedCache/IPublishedMemberCache.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Core/PublishedCache/IPublishedMemberCache.cs b/src/Umbraco.Core/PublishedCache/IPublishedMemberCache.cs index 8a7efdeff31e..fcc1d0010393 100644 --- a/src/Umbraco.Core/PublishedCache/IPublishedMemberCache.cs +++ b/src/Umbraco.Core/PublishedCache/IPublishedMemberCache.cs @@ -5,6 +5,7 @@ namespace Umbraco.Cms.Core.PublishedCache { // TODO: Kill this, why do we want this at all? + // See https://dev.azure.com/umbraco/D-Team%20Tracker/_workitems/edit/11487 public interface IPublishedMemberCache : IXPathNavigable { IPublishedContent GetByProviderKey(object key); From e0abd355f17dad3feef5dca5350fd0241f3ce882 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 15 Apr 2021 21:45:17 +1000 Subject: [PATCH 08/26] adds note --- .../Extensions/ApplicationBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs index 8eddfd644140..0fa16bcd6318 100644 --- a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs @@ -70,7 +70,7 @@ public static IApplicationBuilder UseUmbraco(this IApplicationBuilder app, Actio app.UseSession(); // DO NOT PUT ANY UseEndpoints declarations here!! Those must all come very last in the pipeline, - // endpoints are terminating middleware. + // endpoints are terminating middleware. All of our endpoints are declared in ext of IUmbracoApplicationBuilder app.RunPostPipeline(startupOptions.Value); app.RunPreEndpointsPipeline(startupOptions.Value); From abdb19990c6c8271b4ea8eef2ba867543d7a2bda Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 15 Apr 2021 23:19:36 +1000 Subject: [PATCH 09/26] Cleans up ext methods, ensures that members identity is added on both front-end and back ends. updates how UmbracoApplicationBuilder works in that it explicitly starts endpoints at the time of calling. --- .../Extensions/RuntimeStateExtensions.cs | 13 ++ .../UmbracoTestServerTestBase.cs | 8 +- ...kOfficeServiceCollectionExtensionsTests.cs | 2 +- ...ns.cs => UmbracoBuilder.BackOfficeAuth.cs} | 122 ++++++++++++------ .../UmbracoBuilder.BackOfficeIdentity.cs | 84 ++++++++++++ .../UmbracoBuilderExtensions.cs | 98 +------------- .../UmbracoApplicationBuilder.BackOffice.cs | 9 +- .../UmbracoApplicationBuilder.Preview.cs | 7 +- .../IUmbracoApplicationBuilder.cs | 8 +- .../UmbracoApplicationBuilder.cs | 13 +- .../ServiceCollectionExtensions.cs | 101 --------------- .../UmbracoBuilder.ImageSharp.cs | 76 +++++++++++ .../UmbracoBuilder.MembersIdentity.cs | 49 +++++++ .../UmbracoBuilderExtensions.cs | 4 +- .../ApplicationBuilderExtensions.cs | 16 +-- .../Extensions/IdentityBuilderExtensions.cs | 5 +- ...ions.cs => ServiceCollectionExtensions.cs} | 2 +- .../UmbracoApplicationBuilder.Installer.cs | 9 +- ...oApplicationBuilder.RuntimeMinification.cs | 2 +- .../UmbracoBuilderExtensions.cs | 2 + .../UmbracoApplicationBuilder.Website.cs | 12 +- 21 files changed, 358 insertions(+), 284 deletions(-) create mode 100644 src/Umbraco.Core/Extensions/RuntimeStateExtensions.cs rename src/Umbraco.Web.BackOffice/DependencyInjection/{ServiceCollectionExtensions.cs => UmbracoBuilder.BackOfficeAuth.cs} (77%) create mode 100644 src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs delete mode 100644 src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs create mode 100644 src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs create mode 100644 src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs rename src/Umbraco.Web.Common/Extensions/{UmbracoCoreServiceCollectionExtensions.cs => ServiceCollectionExtensions.cs} (98%) diff --git a/src/Umbraco.Core/Extensions/RuntimeStateExtensions.cs b/src/Umbraco.Core/Extensions/RuntimeStateExtensions.cs new file mode 100644 index 000000000000..4e45ea63d8ba --- /dev/null +++ b/src/Umbraco.Core/Extensions/RuntimeStateExtensions.cs @@ -0,0 +1,13 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Extensions +{ + public static class RuntimeStateExtensions + { + /// + /// Returns true if Umbraco is greater than + /// + public static bool UmbracoCanBoot(this IRuntimeState state) => state.Level > RuntimeLevel.BootFailed; + } +} diff --git a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index a264b74ffcc7..9b0b0b15245f 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -180,9 +180,11 @@ public override void ConfigureServices(IServiceCollection services) public override void Configure(IApplicationBuilder app) { - app.UseUmbraco(); - app.UseUmbracoBackOfficeEndpoints(); - app.UseUmbracoWebsiteEndpoints(); + app.UseUmbraco(u => + { + u.UseBackOfficeEndpoints(); + u.UseWebsiteEndpoints(); + }); } } } diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs index 6cdfe5deb907..f55f96983755 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs @@ -14,7 +14,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.BackOffice [TestFixture] public class UmbracoBackOfficeServiceCollectionExtensionsTests : UmbracoIntegrationTest { - protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.Services.AddUmbracoBackOfficeIdentity(); + protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddBackOfficeIdentity(); [Test] public void AddUmbracoBackOfficeIdentity_ExpectBackOfficeUserStoreResolvable() diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs similarity index 77% rename from src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs rename to src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs index f59f2635e574..bc70a649893f 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs @@ -1,71 +1,107 @@ +using System; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Actions; -using Umbraco.Cms.Core.Net; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Security; -using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Web.BackOffice.Authorization; +using Umbraco.Cms.Web.BackOffice.Middleware; using Umbraco.Cms.Web.BackOffice.Security; -using Umbraco.Cms.Web.Common.AspNetCore; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Cms.Web.Common.Security; +using Umbraco.Cms.Core.Actions; +using Umbraco.Cms.Web.BackOffice.Authorization; + namespace Umbraco.Extensions { - public static class ServiceCollectionExtensions + /// + /// Extension methods for for the Umbraco back office + /// + public static partial class UmbracoBuilderExtensions { /// - /// Adds the services required for using Umbraco back office Identity + /// Adds Umbraco back office authentication requirements /// - public static void AddUmbracoBackOfficeIdentity(this IServiceCollection services) + public static IUmbracoBuilder AddBackOfficeAuthentication(this IUmbracoBuilder builder) { - services.AddDataProtection(); - - services.BuildUmbracoBackOfficeIdentity() - .AddDefaultTokenProviders() - .AddUserStore() - .AddUserManager() - .AddSignInManager() - .AddClaimsPrincipalFactory() - .AddErrorDescriber(); - - // Configure the options specifically for the UmbracoBackOfficeIdentityOptions instance - services.ConfigureOptions(); - services.ConfigureOptions(); + builder.Services + + // This just creates a builder, nothing more + .AddAuthentication() + + // Add our custom schemes which are cookie handlers + .AddCookie(Constants.Security.BackOfficeAuthenticationType) + .AddCookie(Constants.Security.BackOfficeExternalAuthenticationType, o => + { + o.Cookie.Name = Constants.Security.BackOfficeExternalAuthenticationType; + o.ExpireTimeSpan = TimeSpan.FromMinutes(5); + }) + + // Although we don't natively support this, we add it anyways so that if end-users implement the required logic + // they don't have to worry about manually adding this scheme or modifying the sign in manager + .AddCookie(Constants.Security.BackOfficeTwoFactorAuthenticationType, o => + { + o.Cookie.Name = Constants.Security.BackOfficeTwoFactorAuthenticationType; + o.ExpireTimeSpan = TimeSpan.FromMinutes(5); + }); + + builder.Services.ConfigureOptions(); + + builder.Services + .AddSingleton() + .ConfigureOptions(); + + builder.Services.AddUnique(); + builder.Services.AddUnique, PasswordChanger>(); + builder.Services.AddUnique, PasswordChanger>(); + + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + + return builder; } - private static BackOfficeIdentityBuilder BuildUmbracoBackOfficeIdentity(this IServiceCollection services) + /// + /// Adds Umbraco back office authorization policies + /// + public static IUmbracoBuilder AddBackOfficeAuthorizationPolicies(this IUmbracoBuilder builder, string backOfficeAuthenticationScheme = Constants.Security.BackOfficeAuthenticationType) { - services.TryAddScoped(); - services.TryAddSingleton(); - services.TryAddSingleton(); - - return new BackOfficeIdentityBuilder(services); + builder.AddBackOfficeAuthorizationPoliciesInternal(backOfficeAuthenticationScheme); + + builder.Services.AddSingleton(); + + builder.Services.AddAuthorization(options + => options.AddPolicy(AuthorizationPolicies.UmbracoFeatureEnabled, policy + => policy.Requirements.Add(new FeatureAuthorizeRequirement()))); + + return builder; } /// /// Add authorization handlers and policies /// - public static void AddBackOfficeAuthorizationPolicies(this IServiceCollection services, string backOfficeAuthenticationScheme = Constants.Security.BackOfficeAuthenticationType) + private static void AddBackOfficeAuthorizationPoliciesInternal(this IUmbracoBuilder builder, string backOfficeAuthenticationScheme = Constants.Security.BackOfficeAuthenticationType) { // NOTE: Even though we are registering these handlers globally they will only actually execute their logic for // any auth defining a matching requirement and scheme. - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddAuthorization(o => CreatePolicies(o, backOfficeAuthenticationScheme)); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + builder.Services.AddAuthorization(o => CreatePolicies(o, backOfficeAuthenticationScheme)); } private static void CreatePolicies(AuthorizationOptions options, string backOfficeAuthenticationScheme) diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs new file mode 100644 index 000000000000..e401ead59506 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs @@ -0,0 +1,84 @@ +using System; +using System.Linq; +using Ganss.XSS; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Net; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.DependencyInjection; +using Umbraco.Cms.Infrastructure.WebAssets; +using Umbraco.Cms.Web.BackOffice.Controllers; +using Umbraco.Cms.Web.BackOffice.Filters; +using Umbraco.Cms.Web.BackOffice.Middleware; +using Umbraco.Cms.Web.BackOffice.ModelsBuilder; +using Umbraco.Cms.Web.BackOffice.Routing; +using Umbraco.Cms.Web.BackOffice.Security; +using Umbraco.Cms.Web.BackOffice.Services; +using Umbraco.Cms.Web.BackOffice.SignalR; +using Umbraco.Cms.Web.BackOffice.Trees; +using Umbraco.Cms.Web.Common.AspNetCore; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Cms.Web.Common.Security; + +namespace Umbraco.Extensions +{ + /// + /// Extension methods for for the Umbraco back office + /// + public static partial class UmbracoBuilderExtensions + { + /// + /// Adds Identity support for Umbraco back office + /// + public static IUmbracoBuilder AddBackOfficeIdentity(this IUmbracoBuilder builder) + { + IServiceCollection services = builder.Services; + + services.AddDataProtection(); + + builder.BuildUmbracoBackOfficeIdentity() + .AddDefaultTokenProviders() + .AddUserStore() + .AddUserManager() + .AddSignInManager() + .AddClaimsPrincipalFactory() + .AddErrorDescriber(); + + // Configure the options specifically for the UmbracoBackOfficeIdentityOptions instance + services.ConfigureOptions(); + services.ConfigureOptions(); + + return builder; + } + + private static BackOfficeIdentityBuilder BuildUmbracoBackOfficeIdentity(this IUmbracoBuilder builder) + { + IServiceCollection services = builder.Services; + + services.TryAddScoped(); + services.TryAddSingleton(); + services.TryAddSingleton(); + + return new BackOfficeIdentityBuilder(services); + } + + /// + /// Adds support for external login providers in Umbraco + /// + public static IUmbracoBuilder AddBackOfficeExternalLogins(this IUmbracoBuilder umbracoBuilder, Action builder) + { + builder(new BackOfficeExternalLoginsBuilder(umbracoBuilder.Services)); + return umbracoBuilder; + } + + } +} diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index 0e75cf90deb1..da033fc99878 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -2,13 +2,16 @@ using System.Linq; using Ganss.XSS; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Net; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.DependencyInjection; @@ -22,6 +25,7 @@ using Umbraco.Cms.Web.BackOffice.Services; using Umbraco.Cms.Web.BackOffice.SignalR; using Umbraco.Cms.Web.BackOffice.Trees; +using Umbraco.Cms.Web.Common.AspNetCore; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Cms.Web.Common.Security; @@ -30,7 +34,7 @@ namespace Umbraco.Extensions /// /// Extension methods for for the Umbraco back office /// - public static class UmbracoBuilderExtensions + public static partial class UmbracoBuilderExtensions { /// /// Adds all required components to run the Umbraco back office @@ -55,89 +59,6 @@ public static IUmbracoBuilder AddBackOffice(this IUmbracoBuilder builder) => bui .AddUnattedInstallCreateUser() .AddExamine(); - /// - /// Adds Umbraco back office authentication requirements - /// - public static IUmbracoBuilder AddBackOfficeAuthentication(this IUmbracoBuilder builder) - { - builder.Services - - // This just creates a builder, nothing more - .AddAuthentication() - - // Add our custom schemes which are cookie handlers - .AddCookie(Constants.Security.BackOfficeAuthenticationType) - .AddCookie(Constants.Security.BackOfficeExternalAuthenticationType, o => - { - o.Cookie.Name = Constants.Security.BackOfficeExternalAuthenticationType; - o.ExpireTimeSpan = TimeSpan.FromMinutes(5); - }) - - // Although we don't natively support this, we add it anyways so that if end-users implement the required logic - // they don't have to worry about manually adding this scheme or modifying the sign in manager - .AddCookie(Constants.Security.BackOfficeTwoFactorAuthenticationType, o => - { - o.Cookie.Name = Constants.Security.BackOfficeTwoFactorAuthenticationType; - o.ExpireTimeSpan = TimeSpan.FromMinutes(5); - }); - - builder.Services.ConfigureOptions(); - - builder.Services - .AddSingleton() - .ConfigureOptions(); - - builder.Services.AddUnique(); - builder.Services.AddUnique, PasswordChanger>(); - builder.Services.AddUnique, PasswordChanger>(); - - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - - return builder; - } - - /// - /// Adds Identity support for Umbraco back office - /// - public static IUmbracoBuilder AddBackOfficeIdentity(this IUmbracoBuilder builder) - { - builder.Services.AddUmbracoBackOfficeIdentity(); - - return builder; - } - - /// - /// Adds Identity support for Umbraco members - /// - public static IUmbracoBuilder AddMembersIdentity(this IUmbracoBuilder builder) - { - builder.Services.AddMembersIdentity(); - - return builder; - } - - /// - /// Adds Umbraco back office authorization policies - /// - public static IUmbracoBuilder AddBackOfficeAuthorizationPolicies(this IUmbracoBuilder builder, string backOfficeAuthenticationScheme = Constants.Security.BackOfficeAuthenticationType) - { - builder.Services.AddBackOfficeAuthorizationPolicies(backOfficeAuthenticationScheme); - - builder.Services.AddSingleton(); - - builder.Services.AddAuthorization(options - => options.AddPolicy(AuthorizationPolicies.UmbracoFeatureEnabled, policy - => policy.Requirements.Add(new FeatureAuthorizeRequirement()))); - - return builder; - } - /// /// Adds Umbraco preview support /// @@ -148,15 +69,6 @@ public static IUmbracoBuilder AddPreviewSupport(this IUmbracoBuilder builder) return builder; } - /// - /// Adds support for external login providers in Umbraco - /// - public static IUmbracoBuilder AddBackOfficeExternalLogins(this IUmbracoBuilder umbracoBuilder, Action builder) - { - builder(new BackOfficeExternalLoginsBuilder(umbracoBuilder.Services)); - return umbracoBuilder; - } - /// /// Gets the back office tree collection builder /// diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs index 0f5ee616e315..d6fde1503e10 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs @@ -20,16 +20,13 @@ public static IUmbracoApplicationBuilder UseBackOfficeEndpoints(this IUmbracoApp throw new ArgumentNullException(nameof(app)); } - if (!app.AppBuilder.UmbracoCanBoot()) + if (!app.RuntimeState.UmbracoCanBoot()) { return app; } - app.AppBuilder.UseEndpoints(endpoints => - { - BackOfficeAreaRoutes backOfficeRoutes = app.AppBuilder.ApplicationServices.GetRequiredService(); - backOfficeRoutes.CreateRoutes(endpoints); - }); + BackOfficeAreaRoutes backOfficeRoutes = app.ApplicationServices.GetRequiredService(); + backOfficeRoutes.CreateRoutes(app.EndpointRouteBuilder); app.UseUmbracoRuntimeMinificationEndpoints(); app.UseUmbracoPreviewEndpoints(); diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.Preview.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.Preview.cs index fadfbaf78f80..091e48826d25 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.Preview.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.Preview.cs @@ -12,11 +12,8 @@ public static partial class UmbracoApplicationBuilderExtensions { public static IUmbracoApplicationBuilder UseUmbracoPreviewEndpoints(this IUmbracoApplicationBuilder app) { - app.AppBuilder.UseEndpoints(endpoints => - { - PreviewRoutes previewRoutes = app.AppBuilder.ApplicationServices.GetRequiredService(); - previewRoutes.CreateRoutes(endpoints); - }); + PreviewRoutes previewRoutes = app.ApplicationServices.GetRequiredService(); + previewRoutes.CreateRoutes(app.EndpointRouteBuilder); return app; } diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilder.cs b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilder.cs index 20240a569efa..274fc67bc635 100644 --- a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilder.cs +++ b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilder.cs @@ -1,5 +1,7 @@ +using System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; +using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Web.Common.ApplicationBuilder { @@ -8,9 +10,9 @@ namespace Umbraco.Cms.Web.Common.ApplicationBuilder /// public interface IUmbracoApplicationBuilder { - /// - /// Returns the - /// + IRuntimeState RuntimeState { get; } + IServiceProvider ApplicationServices { get; } + IEndpointRouteBuilder EndpointRouteBuilder { get; } IApplicationBuilder AppBuilder { get; } } } diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs index b8a543178600..511b31727e28 100644 --- a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs +++ b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs @@ -1,4 +1,7 @@ -using Microsoft.AspNetCore.Builder; +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Web.Common.ApplicationBuilder { @@ -7,11 +10,17 @@ namespace Umbraco.Cms.Web.Common.ApplicationBuilder ///
internal class UmbracoApplicationBuilder : IUmbracoApplicationBuilder { - public UmbracoApplicationBuilder(IApplicationBuilder appBuilder) + public UmbracoApplicationBuilder(IServiceProvider services, IRuntimeState runtimeState, IApplicationBuilder appBuilder, IEndpointRouteBuilder endpointRouteBuilder) { + ApplicationServices = services; + EndpointRouteBuilder = endpointRouteBuilder; + RuntimeState = runtimeState; AppBuilder = appBuilder; } + public IServiceProvider ApplicationServices { get; } + public IEndpointRouteBuilder EndpointRouteBuilder { get; } + public IRuntimeState RuntimeState { get; } public IApplicationBuilder AppBuilder { get; } } } diff --git a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs deleted file mode 100644 index ad82e1abf527..000000000000 --- a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.Web.Caching; -using SixLabors.ImageSharp.Web.Commands; -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.Security; -using Umbraco.Cms.Web.Common.Security; - -namespace Umbraco.Extensions -{ - public static class ServiceCollectionExtensions - { - /// - /// Adds Image Sharp with Umbraco settings - /// - public static IServiceCollection AddUmbracoImageSharp(this IServiceCollection services, IConfiguration configuration) - { - var imagingSettings = configuration.GetSection(Cms.Core.Constants.Configuration.ConfigImaging) - .Get() ?? new ImagingSettings(); - - services.AddImageSharp(options => - { - options.Configuration = SixLabors.ImageSharp.Configuration.Default; - options.BrowserMaxAge = imagingSettings.Cache.BrowserMaxAge; - options.CacheMaxAge = imagingSettings.Cache.CacheMaxAge; - options.CachedNameLength = imagingSettings.Cache.CachedNameLength; - options.OnParseCommandsAsync = context => - { - RemoveIntParamenterIfValueGreatherThen(context.Commands, ResizeWebProcessor.Width, imagingSettings.Resize.MaxWidth); - RemoveIntParamenterIfValueGreatherThen(context.Commands, ResizeWebProcessor.Height, imagingSettings.Resize.MaxHeight); - - return Task.CompletedTask; - }; - options.OnBeforeSaveAsync = _ => Task.CompletedTask; - options.OnProcessedAsync = _ => Task.CompletedTask; - options.OnPrepareResponseAsync = _ => Task.CompletedTask; - }) - .SetRequestParser() - .SetMemoryAllocator(provider => ArrayPoolMemoryAllocator.CreateWithMinimalPooling()) - .Configure(options => - { - options.CacheFolder = imagingSettings.Cache.CacheFolder; - }) - .SetCache() - .SetCacheHash() - .AddProvider() - .AddProcessor() - .AddProcessor() - .AddProcessor(); - - return services; - } - - /// - /// Adds the services required for using Members Identity - /// - - public static void AddMembersIdentity(this IServiceCollection services) - { - // TODO: We may need to use services.AddIdentityCore instead if this is doing too much - - services.AddIdentity() - .AddDefaultTokenProviders() - .AddUserStore() - .AddRoleStore() - .AddRoleManager() - .AddMemberManager() - .AddSignInManager(); - - services.ConfigureApplicationCookie(x => - { - // TODO: We may want/need to configure these further - - x.LoginPath = null; - x.AccessDeniedPath = null; - x.LogoutPath = null; - }); - } - - private static void RemoveIntParamenterIfValueGreatherThen(IDictionary commands, string parameter, int maxValue) - { - if (commands.TryGetValue(parameter, out var command)) - { - if (int.TryParse(command, out var i)) - { - if (i > maxValue) - { - commands.Remove(parameter); - } - } - } - } - } -} diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs new file mode 100644 index 000000000000..e2b6bba7337e --- /dev/null +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Web.Caching; +using SixLabors.ImageSharp.Web.Commands; +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.DependencyInjection; + +namespace Umbraco.Extensions +{ + public static partial class UmbracoBuilderExtensions + { + /// + /// Adds Image Sharp with Umbraco settings + /// + public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder builder) + { + IConfiguration configuration = builder.Config; + IServiceCollection services = builder.Services; + + ImagingSettings imagingSettings = configuration.GetSection(Cms.Core.Constants.Configuration.ConfigImaging) + .Get() ?? new ImagingSettings(); + + services.AddImageSharp(options => + { + options.Configuration = SixLabors.ImageSharp.Configuration.Default; + options.BrowserMaxAge = imagingSettings.Cache.BrowserMaxAge; + options.CacheMaxAge = imagingSettings.Cache.CacheMaxAge; + options.CachedNameLength = imagingSettings.Cache.CachedNameLength; + options.OnParseCommandsAsync = context => + { + RemoveIntParamenterIfValueGreatherThen(context.Commands, ResizeWebProcessor.Width, imagingSettings.Resize.MaxWidth); + RemoveIntParamenterIfValueGreatherThen(context.Commands, ResizeWebProcessor.Height, imagingSettings.Resize.MaxHeight); + + return Task.CompletedTask; + }; + options.OnBeforeSaveAsync = _ => Task.CompletedTask; + options.OnProcessedAsync = _ => Task.CompletedTask; + options.OnPrepareResponseAsync = _ => Task.CompletedTask; + }) + .SetRequestParser() + .SetMemoryAllocator(provider => ArrayPoolMemoryAllocator.CreateWithMinimalPooling()) + .Configure(options => + { + options.CacheFolder = imagingSettings.Cache.CacheFolder; + }) + .SetCache() + .SetCacheHash() + .AddProvider() + .AddProcessor() + .AddProcessor() + .AddProcessor(); + + return services; + } + + private static void RemoveIntParamenterIfValueGreatherThen(IDictionary commands, string parameter, int maxValue) + { + if (commands.TryGetValue(parameter, out var command)) + { + if (int.TryParse(command, out var i)) + { + if (i > maxValue) + { + commands.Remove(parameter); + } + } + } + } + } +} diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs new file mode 100644 index 000000000000..7521fc8f1724 --- /dev/null +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs @@ -0,0 +1,49 @@ +using System.Linq; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Web.Common.Security; + +namespace Umbraco.Extensions +{ + public static partial class UmbracoBuilderExtensions + { + /// + /// Adds Identity support for Umbraco members + /// + public static IUmbracoBuilder AddMembersIdentity(this IUmbracoBuilder builder) + { + IServiceCollection services = builder.Services; + + // check if this has already been added, we cannot add twice but both front-end and back end + // depend on this so it's possible it can be called twice. + var distCacheBinder = new UniqueServiceDescriptor(typeof(IMemberManager), typeof(MemberManager), ServiceLifetime.Scoped); + if (builder.Services.Contains(distCacheBinder)) + { + return builder; + } + + // TODO: We may need to use services.AddIdentityCore instead if this is doing too much + + services.AddIdentity() + .AddDefaultTokenProviders() + .AddUserStore() + .AddRoleStore() + .AddRoleManager() + .AddMemberManager() + .AddSignInManager(); + + services.ConfigureApplicationCookie(x => + { + // TODO: We may want/need to configure these further + + x.LoginPath = null; + x.AccessDeniedPath = null; + x.LogoutPath = null; + }); + + return builder; + } + } +} diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index fb748403954f..5f2326fd5d8d 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -67,7 +67,7 @@ namespace Umbraco.Extensions /// /// Extension methods for for the common Umbraco functionality /// - public static class UmbracoBuilderExtensions + public static partial class UmbracoBuilderExtensions { /// /// Creates an and registers basic Umbraco services @@ -266,7 +266,7 @@ public static IUmbracoBuilder AddWebComponents(this IUmbracoBuilder builder) builder.Services.TryAddEnumerable(ServiceDescriptor.Transient()); builder.Services.TryAddEnumerable(ServiceDescriptor.Transient()); builder.Services.TryAddEnumerable(ServiceDescriptor.Transient()); - builder.Services.AddUmbracoImageSharp(builder.Config); + builder.AddUmbracoImageSharp(); // AspNetCore specific services builder.Services.AddUnique(); diff --git a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs index 0fa16bcd6318..1c41f520a7ef 100644 --- a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs @@ -78,8 +78,13 @@ public static IApplicationBuilder UseUmbraco(this IApplicationBuilder app, Actio // create our custom builder and execute the callback // which will allow executing all IUmbracoApplicationBuilder ext methods // to create endpoints. - var umbAppBuilder = new UmbracoApplicationBuilder(app); - configureUmbraco(umbAppBuilder); + app.UseEndpoints(endpoints => + { + var umbAppBuilder = (IUmbracoApplicationBuilder)ActivatorUtilities.CreateInstance( + app.ApplicationServices, + new object[] { app, endpoints }); + configureUmbraco(umbAppBuilder); + }); return app; } @@ -112,12 +117,7 @@ private static void RunPreEndpointsPipeline(this IApplicationBuilder app, Umbrac /// Returns true if Umbraco is greater than /// public static bool UmbracoCanBoot(this IApplicationBuilder app) - { - var state = app.ApplicationServices.GetRequiredService(); - - // can't continue if boot failed - return state.Level > RuntimeLevel.BootFailed; - } + => app.ApplicationServices.GetRequiredService().UmbracoCanBoot(); /// /// Enables core Umbraco functionality diff --git a/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs index 14cd644a660d..77b9f6c8ddc0 100644 --- a/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Security; namespace Umbraco.Extensions @@ -19,7 +20,9 @@ public static IdentityBuilder AddMemberManager(this Id where TUserManager : UserManager, TInterface { identityBuilder.AddUserManager(); - identityBuilder.Services.AddScoped(typeof(TInterface), typeof(TUserManager)); + // use a UniqueServiceDescriptor so we can check if it's already been added + var memberManagerDescriptor = new UniqueServiceDescriptor(typeof(TInterface), typeof(TUserManager), ServiceLifetime.Scoped); + identityBuilder.Services.Add(memberManagerDescriptor); identityBuilder.Services.AddScoped(typeof(UserManager), factory => factory.GetRequiredService()); return identityBuilder; } diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs similarity index 98% rename from src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs rename to src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs index 75a177f25a6e..7fe337a9f9f2 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs @@ -16,7 +16,7 @@ namespace Umbraco.Extensions { - public static class UmbracoCoreServiceCollectionExtensions + public static partial class ServiceCollectionExtensions { /// /// Create and configure the logger diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.Installer.cs b/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.Installer.cs index 3adf7a9995c7..8c92f189cf97 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.Installer.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.Installer.cs @@ -15,16 +15,13 @@ public static partial class UmbracoApplicationBuilderExtensions /// public static IUmbracoApplicationBuilder UseInstallerEndpoints(this IUmbracoApplicationBuilder app) { - if (!app.AppBuilder.UmbracoCanBoot()) + if (!app.RuntimeState.UmbracoCanBoot()) { return app; } - app.AppBuilder.UseEndpoints(endpoints => - { - InstallAreaRoutes installerRoutes = app.AppBuilder.ApplicationServices.GetRequiredService(); - installerRoutes.CreateRoutes(endpoints); - }); + InstallAreaRoutes installerRoutes = app.ApplicationServices.GetRequiredService(); + installerRoutes.CreateRoutes(app.EndpointRouteBuilder); return app; } diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.RuntimeMinification.cs b/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.RuntimeMinification.cs index 0d962dd5a588..550c85d6a574 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.RuntimeMinification.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.RuntimeMinification.cs @@ -18,7 +18,7 @@ public static IUmbracoApplicationBuilder UseUmbracoRuntimeMinificationEndpoints( throw new ArgumentNullException(nameof(app)); } - if (!app.AppBuilder.UmbracoCanBoot()) + if (!app.RuntimeState.UmbracoCanBoot()) { return app; } diff --git a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs index cebc164c041a..6f7a119c1bb6 100644 --- a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs @@ -55,6 +55,8 @@ public static IUmbracoBuilder AddWebsite(this IUmbracoBuilder builder) .AddDistributedCache() .AddModelsBuilder(); + builder.AddMembersIdentity(); + return builder; } } diff --git a/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs b/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs index 35d014f9157f..ad8839ac693a 100644 --- a/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs +++ b/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs @@ -21,18 +21,14 @@ public static IUmbracoApplicationBuilder UseWebsiteEndpoints(this IUmbracoApplic throw new ArgumentNullException(nameof(app)); } - if (!app.AppBuilder.UmbracoCanBoot()) + if (!app.RuntimeState.UmbracoCanBoot()) { return app; } - app.AppBuilder.UseEndpoints(endpoints => - { - FrontEndRoutes surfaceRoutes = app.AppBuilder.ApplicationServices.GetRequiredService(); - surfaceRoutes.CreateRoutes(endpoints); - - endpoints.MapDynamicControllerRoute("/{**slug}"); - }); + FrontEndRoutes surfaceRoutes = app.ApplicationServices.GetRequiredService(); + surfaceRoutes.CreateRoutes(app.EndpointRouteBuilder); + app.EndpointRouteBuilder.MapDynamicControllerRoute("/{**slug}"); return app; } From 3dfd4d7ed4cffe4b8cc5bfadc8cbc1b8a6cf5db8 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 15 Apr 2021 23:31:23 +1000 Subject: [PATCH 10/26] Changes name to IUmbracoEndpointBuilder --- .../Extensions/UmbracoApplicationBuilder.BackOffice.cs | 4 ++-- .../Extensions/UmbracoApplicationBuilder.Preview.cs | 4 ++-- ...mbracoApplicationBuilder.cs => IUmbracoEndpointBuilder.cs} | 2 +- ...UmbracoApplicationBuilder.cs => UmbracoEndpointBuilder.cs} | 4 ++-- .../Extensions/ApplicationBuilderExtensions.cs | 4 ++-- .../Extensions/UmbracoApplicationBuilder.Installer.cs | 2 +- .../UmbracoApplicationBuilder.RuntimeMinification.cs | 2 +- .../Extensions/UmbracoApplicationBuilder.Website.cs | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) rename src/Umbraco.Web.Common/ApplicationBuilder/{IUmbracoApplicationBuilder.cs => IUmbracoEndpointBuilder.cs} (91%) rename src/Umbraco.Web.Common/ApplicationBuilder/{UmbracoApplicationBuilder.cs => UmbracoEndpointBuilder.cs} (75%) diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs index d6fde1503e10..db7cf0b311e7 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs @@ -8,11 +8,11 @@ namespace Umbraco.Extensions { /// - /// extensions for Umbraco + /// extensions for Umbraco /// public static partial class UmbracoApplicationBuilderExtensions { - public static IUmbracoApplicationBuilder UseBackOfficeEndpoints(this IUmbracoApplicationBuilder app) + public static IUmbracoEndpointBuilder UseBackOfficeEndpoints(this IUmbracoEndpointBuilder app) { // NOTE: This method will have been called after UseRouting, UseAuthentication, UseAuthorization if (app == null) diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.Preview.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.Preview.cs index 091e48826d25..014f81fe8cf3 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.Preview.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.Preview.cs @@ -6,11 +6,11 @@ namespace Umbraco.Extensions { /// - /// extensions for Umbraco + /// extensions for Umbraco /// public static partial class UmbracoApplicationBuilderExtensions { - public static IUmbracoApplicationBuilder UseUmbracoPreviewEndpoints(this IUmbracoApplicationBuilder app) + public static IUmbracoEndpointBuilder UseUmbracoPreviewEndpoints(this IUmbracoEndpointBuilder app) { PreviewRoutes previewRoutes = app.ApplicationServices.GetRequiredService(); previewRoutes.CreateRoutes(app.EndpointRouteBuilder); diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilder.cs b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoEndpointBuilder.cs similarity index 91% rename from src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilder.cs rename to src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoEndpointBuilder.cs index 274fc67bc635..657a4d42e6e5 100644 --- a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilder.cs +++ b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoEndpointBuilder.cs @@ -8,7 +8,7 @@ namespace Umbraco.Cms.Web.Common.ApplicationBuilder /// /// A builder to allow encapsulating the enabled routing features in Umbraco /// - public interface IUmbracoApplicationBuilder + public interface IUmbracoEndpointBuilder { IRuntimeState RuntimeState { get; } IServiceProvider ApplicationServices { get; } diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoEndpointBuilder.cs similarity index 75% rename from src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs rename to src/Umbraco.Web.Common/ApplicationBuilder/UmbracoEndpointBuilder.cs index 511b31727e28..47f6b8e66715 100644 --- a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs +++ b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoEndpointBuilder.cs @@ -8,9 +8,9 @@ namespace Umbraco.Cms.Web.Common.ApplicationBuilder /// /// A builder to allow encapsulating the enabled routing features in Umbraco /// - internal class UmbracoApplicationBuilder : IUmbracoApplicationBuilder + internal class UmbracoEndpointBuilder : IUmbracoEndpointBuilder { - public UmbracoApplicationBuilder(IServiceProvider services, IRuntimeState runtimeState, IApplicationBuilder appBuilder, IEndpointRouteBuilder endpointRouteBuilder) + public UmbracoEndpointBuilder(IServiceProvider services, IRuntimeState runtimeState, IApplicationBuilder appBuilder, IEndpointRouteBuilder endpointRouteBuilder) { ApplicationServices = services; EndpointRouteBuilder = endpointRouteBuilder; diff --git a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs index 1c41f520a7ef..55e02b8aca3b 100644 --- a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs @@ -25,7 +25,7 @@ public static class ApplicationBuilderExtensions /// /// Configures and use services required for using Umbraco /// - public static IApplicationBuilder UseUmbraco(this IApplicationBuilder app, Action configureUmbraco) + public static IApplicationBuilder UseUmbraco(this IApplicationBuilder app, Action configureUmbraco) { IOptions startupOptions = app.ApplicationServices.GetRequiredService>(); @@ -80,7 +80,7 @@ public static IApplicationBuilder UseUmbraco(this IApplicationBuilder app, Actio // to create endpoints. app.UseEndpoints(endpoints => { - var umbAppBuilder = (IUmbracoApplicationBuilder)ActivatorUtilities.CreateInstance( + var umbAppBuilder = (IUmbracoEndpointBuilder)ActivatorUtilities.CreateInstance( app.ApplicationServices, new object[] { app, endpoints }); configureUmbraco(umbAppBuilder); diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.Installer.cs b/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.Installer.cs index 8c92f189cf97..3859814775bd 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.Installer.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.Installer.cs @@ -13,7 +13,7 @@ public static partial class UmbracoApplicationBuilderExtensions /// /// Enables the Umbraco installer /// - public static IUmbracoApplicationBuilder UseInstallerEndpoints(this IUmbracoApplicationBuilder app) + public static IUmbracoEndpointBuilder UseInstallerEndpoints(this IUmbracoEndpointBuilder app) { if (!app.RuntimeState.UmbracoCanBoot()) { diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.RuntimeMinification.cs b/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.RuntimeMinification.cs index 550c85d6a574..0d8c7df72b40 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.RuntimeMinification.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.RuntimeMinification.cs @@ -11,7 +11,7 @@ public static partial class UmbracoApplicationBuilderExtensions /// /// Enables runtime minification for Umbraco /// - public static IUmbracoApplicationBuilder UseUmbracoRuntimeMinificationEndpoints(this IUmbracoApplicationBuilder app) + public static IUmbracoEndpointBuilder UseUmbracoRuntimeMinificationEndpoints(this IUmbracoEndpointBuilder app) { if (app == null) { diff --git a/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs b/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs index ad8839ac693a..ac40cd7f4b32 100644 --- a/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs +++ b/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs @@ -14,7 +14,7 @@ public static partial class UmbracoApplicationBuilderExtensions /// /// Sets up routes for the front-end umbraco website /// - public static IUmbracoApplicationBuilder UseWebsiteEndpoints(this IUmbracoApplicationBuilder app) + public static IUmbracoEndpointBuilder UseWebsiteEndpoints(this IUmbracoEndpointBuilder app) { if (app == null) { From cf770a5b89550d693705be4f5eb0b18c9fe43d49 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 15 Apr 2021 23:32:02 +1000 Subject: [PATCH 11/26] adds note --- .../ApplicationBuilder/UmbracoEndpointBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoEndpointBuilder.cs b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoEndpointBuilder.cs index 47f6b8e66715..56d856a22aa3 100644 --- a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoEndpointBuilder.cs +++ b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoEndpointBuilder.cs @@ -6,7 +6,7 @@ namespace Umbraco.Cms.Web.Common.ApplicationBuilder { /// - /// A builder to allow encapsulating the enabled routing features in Umbraco + /// A builder to allow encapsulating the enabled endpoints in Umbraco /// internal class UmbracoEndpointBuilder : IUmbracoEndpointBuilder { From cfcbe3695588326ebe7043cc50a746e36e381357 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 16 Apr 2021 01:07:05 +1000 Subject: [PATCH 12/26] Fixing tests, fixing error describers so there's 2x one for back office, one for members, fixes TryConvertTo, fixes login redirect --- .../Extensions/ObjectExtensions.cs | 20 +++++++++++++++++-- ...scriber.cs => BackOfficeErrorDescriber.cs} | 7 ++++++- .../Security/BackOfficeIdentityBuilder.cs | 6 +++--- .../Security/BackOfficeUserStore.cs | 2 +- .../Security/MemberManagerTests.cs | 2 +- .../Security/MemberSignInManagerTests.cs | 8 ++++++-- .../UmbracoBuilder.BackOfficeIdentity.cs | 2 +- .../UmbracoBuilder.MembersIdentity.cs | 3 ++- .../Security/BackOfficeUserManager.cs | 2 +- .../Security/MemberManager.cs | 2 +- .../Controllers/UmbLoginController.cs | 7 +++++-- 11 files changed, 45 insertions(+), 16 deletions(-) rename src/Umbraco.Infrastructure/Security/{BackOfficeIdentityErrorDescriber.cs => BackOfficeErrorDescriber.cs} (55%) diff --git a/src/Umbraco.Core/Extensions/ObjectExtensions.cs b/src/Umbraco.Core/Extensions/ObjectExtensions.cs index e2cb09c97838..db2d63b643a7 100644 --- a/src/Umbraco.Core/Extensions/ObjectExtensions.cs +++ b/src/Umbraco.Core/Extensions/ObjectExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; @@ -76,10 +76,26 @@ public static T SafeCast(this object input) /// The public static Attempt TryConvertTo(this object input) { - var result = TryConvertTo(input, typeof(T)); + Attempt result = TryConvertTo(input, typeof(T)); if (result.Success) + { return Attempt.Succeed((T)result.Result); + } + + if (input == null) + { + if (typeof(T).IsValueType) + { + // fail, cannot convert null to a value type + return Attempt.Fail(); + } + else + { + // sure, null can be any object + return Attempt.Succeed((T)input); + } + } // just try to cast try diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityErrorDescriber.cs b/src/Umbraco.Infrastructure/Security/BackOfficeErrorDescriber.cs similarity index 55% rename from src/Umbraco.Infrastructure/Security/BackOfficeIdentityErrorDescriber.cs rename to src/Umbraco.Infrastructure/Security/BackOfficeErrorDescriber.cs index 67287e985839..60d913f7c5ba 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityErrorDescriber.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeErrorDescriber.cs @@ -5,7 +5,12 @@ namespace Umbraco.Cms.Core.Security /// /// Umbraco back office specific /// - public class BackOfficeIdentityErrorDescriber : IdentityErrorDescriber + public class BackOfficeErrorDescriber : IdentityErrorDescriber + { + // TODO: Override all the methods in order to provide our own translated error messages + } + + public class MembersErrorDescriber : IdentityErrorDescriber { // TODO: Override all the methods in order to provide our own translated error messages } diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityBuilder.cs b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityBuilder.cs index 4e2c1f2704e1..9b1f581b8281 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityBuilder.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityBuilder.cs @@ -44,14 +44,14 @@ private void InitializeServices(IServiceCollection services) services.TryAddScoped, DefaultUserConfirmation>(); } + // override to add itself, by default identity only wants a single IdentityErrorDescriber public override IdentityBuilder AddErrorDescriber() { - if (!typeof(BackOfficeIdentityErrorDescriber).IsAssignableFrom(typeof(TDescriber))) + if (!typeof(BackOfficeErrorDescriber).IsAssignableFrom(typeof(TDescriber))) { - throw new InvalidOperationException($"The type {typeof(TDescriber)} does not inherit from {typeof(BackOfficeIdentityErrorDescriber)}"); + throw new InvalidOperationException($"The type {typeof(TDescriber)} does not inherit from {typeof(BackOfficeErrorDescriber)}"); } - // Add as itself, by default identity only wants a single IdentityErrorDescriber Services.AddScoped(); return this; } diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs index 489fab835d62..04a7b12aec50 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs @@ -44,7 +44,7 @@ public BackOfficeUserStore( IExternalLoginService externalLoginService, IOptions globalSettings, UmbracoMapper mapper, - BackOfficeIdentityErrorDescriber describer, + BackOfficeErrorDescriber describer, AppCaches appCaches) : base(describer) { diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs index 0f4e2c920a06..7a345e8edab1 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs @@ -70,7 +70,7 @@ public MemberManager CreateSut() _mockPasswordHasher.Object, userValidators, pwdValidators, - new BackOfficeIdentityErrorDescriber(), + new MembersErrorDescriber(), _mockServiceProviders.Object, new Mock>>().Object, _mockPasswordConfiguration.Object, 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 c5d24e6bd937..f33344e49b21 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs @@ -11,8 +11,10 @@ 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.Core.Services; using Umbraco.Cms.Web.Common.Security; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security @@ -63,10 +65,12 @@ private static Mock MockMemberManager() Mock.Of>(), Enumerable.Empty>(), Enumerable.Empty>(), - new BackOfficeIdentityErrorDescriber(), + new MembersErrorDescriber(), Mock.Of(), Mock.Of>>(), - Options.Create(new Cms.Core.Configuration.Models.MemberPasswordConfigurationSettings())); + Options.Create(new MemberPasswordConfigurationSettings()), + Mock.Of(), + Mock.Of()); [Test] public async Task WhenPasswordSignInAsyncIsCalled_AndEverythingIsSetup_ThenASignInResultSucceededShouldBeReturnedAsync() diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs index e401ead59506..f9b0654096b2 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs @@ -51,7 +51,7 @@ public static IUmbracoBuilder AddBackOfficeIdentity(this IUmbracoBuilder builder .AddUserManager() .AddSignInManager() .AddClaimsPrincipalFactory() - .AddErrorDescriber(); + .AddErrorDescriber(); // Configure the options specifically for the UmbracoBackOfficeIdentityOptions instance services.ConfigureOptions(); diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs index 7521fc8f1724..75d2114cf876 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs @@ -32,7 +32,8 @@ public static IUmbracoBuilder AddMembersIdentity(this IUmbracoBuilder builder) .AddRoleStore() .AddRoleManager() .AddMemberManager() - .AddSignInManager(); + .AddSignInManager() + .AddErrorDescriber(); services.ConfigureApplicationCookie(x => { diff --git a/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs index 459ed5713867..635697940984 100644 --- a/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs @@ -28,7 +28,7 @@ public BackOfficeUserManager( IPasswordHasher passwordHasher, IEnumerable> userValidators, IEnumerable> passwordValidators, - BackOfficeIdentityErrorDescriber errors, + BackOfficeErrorDescriber errors, IServiceProvider services, IHttpContextAccessor httpContextAccessor, ILogger> logger, diff --git a/src/Umbraco.Web.Common/Security/MemberManager.cs b/src/Umbraco.Web.Common/Security/MemberManager.cs index 873f335ec144..0ea7f2e74c4e 100644 --- a/src/Umbraco.Web.Common/Security/MemberManager.cs +++ b/src/Umbraco.Web.Common/Security/MemberManager.cs @@ -28,7 +28,7 @@ public MemberManager( IPasswordHasher passwordHasher, IEnumerable> userValidators, IEnumerable> passwordValidators, - BackOfficeIdentityErrorDescriber errors, + IdentityErrorDescriber errors, IServiceProvider services, ILogger> logger, IOptions passwordConfiguration, diff --git a/src/Umbraco.Web.Website/Controllers/UmbLoginController.cs b/src/Umbraco.Web.Website/Controllers/UmbLoginController.cs index 6a7b8941be07..afeb41a25279 100644 --- a/src/Umbraco.Web.Website/Controllers/UmbLoginController.cs +++ b/src/Umbraco.Web.Website/Controllers/UmbLoginController.cs @@ -65,8 +65,11 @@ public async Task HandleLogin([Bind(Prefix = "loginModel")]LoginM : CurrentPage.AncestorOrSelf(1).Url(PublishedUrlProvider)); } - // Redirect to current page by default. - return RedirectToCurrentUmbracoPage(); + // Redirect to current URL by default. + // This is different from the current 'page' because when using Public Access the current page + // will be the login page, but the URL will be on the requested page so that's where we need + // to redirect too. + return RedirectToCurrentUmbracoUrl(); } if (result.RequiresTwoFactor) From 583a15c89734355c02669b1aa2bd17c5d01779e0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 16 Apr 2021 01:29:24 +1000 Subject: [PATCH 13/26] fixing build --- .../MembersServiceCollectionExtensionsTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.Common/MembersServiceCollectionExtensionsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.Common/MembersServiceCollectionExtensionsTests.cs index e76716c152c9..28c61a743cdd 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Web.Common/MembersServiceCollectionExtensionsTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.Common/MembersServiceCollectionExtensionsTests.cs @@ -11,7 +11,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Web.Common [TestFixture] public class MembersServiceCollectionExtensionsTests : UmbracoIntegrationTest { - protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.Services.AddMembersIdentity(); + protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddMembersIdentity(); [Test] public void AddMembersIdentity_ExpectMembersUserStoreResolvable() From a4007eb00fa1fd2740a2bc36e32641d856aad922 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 19 Apr 2021 18:19:19 +1000 Subject: [PATCH 14/26] Fixes keepalive, fixes PublicAccessMiddleware to not throw, updates startup code to be more clear and removes magic that registers middleware. --- .../Configuration/Models/GlobalSettings.cs | 2 +- .../Configuration/Models/KeepAliveSettings.cs | 5 ++- src/Umbraco.Core/Routing/WebPath.cs | 2 -- .../HostedServices/KeepAlive.cs | 24 ++++++------- .../Controllers/KeepAliveController.cs | 35 ------------------- .../UmbracoBuilder.BackOfficeAuth.cs | 4 +-- .../UmbracoBuilderExtensions.cs | 2 ++ .../UmbracoApplicationBuilder.BackOffice.cs | 21 +++++++++++ .../Filters/OnlyLocalRequestsAttribute.cs | 17 --------- ...iceExternalLoginProviderErrorMiddleware.cs | 17 ++------- ...igureGlobalOptionsForKeepAliveMiddlware.cs | 22 ++++++++++++ .../Middleware/KeepAliveMiddleware.cs | 26 ++++++++++++++ .../IUmbracoApplicationBuilder.cs | 19 ++++++++++ .../IUmbracoEndpointBuilder.cs | 1 + .../UmbracoApplicationBuilder.cs | 33 +++++++++++++++++ .../ApplicationBuilderExtensions.cs | 17 +++------ src/Umbraco.Web.UI.NetCore/Startup.cs | 17 +++++---- .../UmbracoBuilderExtensions.cs | 4 +-- .../UmbracoApplicationBuilder.Website.cs | 30 +++++++++++----- .../Middleware/PublicAccessMiddleware.cs | 21 ++--------- 20 files changed, 181 insertions(+), 138 deletions(-) delete mode 100644 src/Umbraco.Web.BackOffice/Controllers/KeepAliveController.cs delete mode 100644 src/Umbraco.Web.BackOffice/Filters/OnlyLocalRequestsAttribute.cs create mode 100644 src/Umbraco.Web.BackOffice/Middleware/ConfigureGlobalOptionsForKeepAliveMiddlware.cs create mode 100644 src/Umbraco.Web.BackOffice/Middleware/KeepAliveMiddleware.cs create mode 100644 src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilder.cs create mode 100644 src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs diff --git a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs index a31edd5a030b..b20ac847006b 100644 --- a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs @@ -14,7 +14,7 @@ internal const string StaticReservedPaths = "~/app_plugins/,~/install/,~/mini-profiler-resources/,~/umbraco/,"; // must end with a comma! internal const string - StaticReservedUrls = "~/config/splashes/noNodes.aspx,~/.well-known,"; // must end with a comma! + StaticReservedUrls = "~/.well-known,"; // must end with a comma! /// /// Gets or sets a value for the reserved URLs. diff --git a/src/Umbraco.Core/Configuration/Models/KeepAliveSettings.cs b/src/Umbraco.Core/Configuration/Models/KeepAliveSettings.cs index 831ad8d84d8f..898798588c63 100644 --- a/src/Umbraco.Core/Configuration/Models/KeepAliveSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/KeepAliveSettings.cs @@ -1,6 +1,9 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System; +using Umbraco.Cms.Core.Hosting; + namespace Umbraco.Cms.Core.Configuration.Models { /// @@ -16,6 +19,6 @@ public class KeepAliveSettings /// /// Gets a value for the keep alive ping URL. /// - public string KeepAlivePingUrl => "{umbracoApplicationUrl}/api/keepalive/ping"; + public string KeepAlivePingUrl => "~/api/keepalive/ping"; } } diff --git a/src/Umbraco.Core/Routing/WebPath.cs b/src/Umbraco.Core/Routing/WebPath.cs index 503cbb27fd4b..acc5e323c179 100644 --- a/src/Umbraco.Core/Routing/WebPath.cs +++ b/src/Umbraco.Core/Routing/WebPath.cs @@ -13,8 +13,6 @@ public static string Combine(params string[] paths) if (paths == null) throw new ArgumentNullException(nameof(paths)); if (!paths.Any()) return string.Empty; - - var result = paths[0].TrimEnd(separator); if(!(result.StartsWith(separator) || result.StartsWith("~" + separator))) diff --git a/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs index 5ceeed975552..40c250f86e90 100644 --- a/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs +++ b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs @@ -2,14 +2,15 @@ // See LICENSE for more details. using System; +using System.IO; using System.Net.Http; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Logging; +using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Runtime; using Umbraco.Cms.Core.Sync; using Umbraco.Extensions; @@ -85,21 +86,18 @@ public override async Task PerformExecuteAsync(object state) using (_profilingLogger.DebugDuration("Keep alive executing", "Keep alive complete")) { - var keepAlivePingUrl = _keepAliveSettings.KeepAlivePingUrl; - try + var umbracoAppUrl = _hostingEnvironment.ApplicationMainUrl.ToString(); + if (umbracoAppUrl.IsNullOrWhiteSpace()) { - if (keepAlivePingUrl.Contains("{umbracoApplicationUrl}")) - { - var umbracoAppUrl = _hostingEnvironment.ApplicationMainUrl.ToString(); - if (umbracoAppUrl.IsNullOrWhiteSpace()) - { - _logger.LogWarning("No umbracoApplicationUrl for service (yet), skip."); - return; - } + _logger.LogWarning("No umbracoApplicationUrl for service (yet), skip."); + return; + } - keepAlivePingUrl = keepAlivePingUrl.Replace("{umbracoApplicationUrl}", umbracoAppUrl.TrimEnd(Constants.CharArrays.ForwardSlash)); - } + // If the config is an absolute path, just use it + string keepAlivePingUrl = WebPath.Combine(umbracoAppUrl, _hostingEnvironment.ToAbsolute(_keepAliveSettings.KeepAlivePingUrl)); + try + { var request = new HttpRequestMessage(HttpMethod.Get, keepAlivePingUrl); HttpClient httpClient = _httpClientFactory.CreateClient(); _ = await httpClient.SendAsync(request); diff --git a/src/Umbraco.Web.BackOffice/Controllers/KeepAliveController.cs b/src/Umbraco.Web.BackOffice/Controllers/KeepAliveController.cs deleted file mode 100644 index 650efec70f44..000000000000 --- a/src/Umbraco.Web.BackOffice/Controllers/KeepAliveController.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Runtime.Serialization; -using Microsoft.AspNetCore.Mvc; -using Umbraco.Cms.Web.BackOffice.Filters; -using Umbraco.Cms.Web.Common.Attributes; -using Umbraco.Cms.Web.Common.Controllers; -using Constants = Umbraco.Cms.Core.Constants; - -namespace Umbraco.Cms.Web.BackOffice.Controllers -{ - [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] - [IsBackOffice] - public class KeepAliveController : UmbracoApiController - { - [OnlyLocalRequests] - [HttpGet] - public KeepAlivePingResult Ping() - { - return new KeepAlivePingResult - { - Success = true, - Message = "I'm alive!" - }; - } - } - - - public class KeepAlivePingResult - { - [DataMember(Name = "success")] - public bool Success { get; set; } - - [DataMember(Name = "message")] - public string Message { get; set; } - } -} diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs index bc70a649893f..d348ccdfc0dd 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs @@ -47,9 +47,7 @@ public static IUmbracoBuilder AddBackOfficeAuthentication(this IUmbracoBuilder b builder.Services.ConfigureOptions(); - builder.Services - .AddSingleton() - .ConfigureOptions(); + builder.Services.AddSingleton(); builder.Services.AddUnique(); builder.Services.AddUnique, PasswordChanger>(); diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index da033fc99878..63f04137da7e 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -77,6 +77,8 @@ public static TreeCollectionBuilder Trees(this IUmbracoBuilder builder) public static IUmbracoBuilder AddBackOfficeCore(this IUmbracoBuilder builder) { + builder.Services.AddSingleton(); + builder.Services.ConfigureOptions(); builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs index db7cf0b311e7..dc113a99b089 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs @@ -1,6 +1,10 @@ using System; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Web.BackOffice.Middleware; using Umbraco.Cms.Web.BackOffice.Routing; using Umbraco.Cms.Web.Common.ApplicationBuilder; using Umbraco.Cms.Web.Common.Extensions; @@ -12,6 +16,23 @@ namespace Umbraco.Extensions /// public static partial class UmbracoApplicationBuilderExtensions { + /// + /// Adds all required middleware to run the back office + /// + /// + /// + public static IUmbracoApplicationBuilder WithBackOffice(this IUmbracoApplicationBuilder builder) + { + KeepAliveSettings keepAliveSettings = builder.ApplicationServices.GetRequiredService>().Value; + IHostingEnvironment hostingEnvironment = builder.ApplicationServices.GetRequiredService(); + builder.AppBuilder.Map( + hostingEnvironment.ToAbsolute(keepAliveSettings.KeepAlivePingUrl), + a => a.UseMiddleware()); + + builder.AppBuilder.UseMiddleware(); + return builder; + } + public static IUmbracoEndpointBuilder UseBackOfficeEndpoints(this IUmbracoEndpointBuilder app) { // NOTE: This method will have been called after UseRouting, UseAuthentication, UseAuthorization diff --git a/src/Umbraco.Web.BackOffice/Filters/OnlyLocalRequestsAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/OnlyLocalRequestsAttribute.cs deleted file mode 100644 index d811287b85d1..000000000000 --- a/src/Umbraco.Web.BackOffice/Filters/OnlyLocalRequestsAttribute.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Web.BackOffice.Filters -{ - public class OnlyLocalRequestsAttribute : ActionFilterAttribute - { - public override void OnActionExecuting(ActionExecutingContext context) - { - if (!context.HttpContext.Request.IsLocal()) - { - context.Result = new NotFoundResult(); - } - } - } -} diff --git a/src/Umbraco.Web.BackOffice/Middleware/BackOfficeExternalLoginProviderErrorMiddleware.cs b/src/Umbraco.Web.BackOffice/Middleware/BackOfficeExternalLoginProviderErrorMiddleware.cs index 9838660663a3..b093611282c4 100644 --- a/src/Umbraco.Web.BackOffice/Middleware/BackOfficeExternalLoginProviderErrorMiddleware.cs +++ b/src/Umbraco.Web.BackOffice/Middleware/BackOfficeExternalLoginProviderErrorMiddleware.cs @@ -1,17 +1,15 @@ using System; using System.Text; using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.Extensions.Options; using Newtonsoft.Json; -using Umbraco.Cms.Web.Common.ApplicationBuilder; using Umbraco.Extensions; using HttpRequestExtensions = Umbraco.Extensions.HttpRequestExtensions; namespace Umbraco.Cms.Web.BackOffice.Middleware { + /// /// Used to handle errors registered by external login providers /// @@ -19,19 +17,8 @@ namespace Umbraco.Cms.Web.BackOffice.Middleware /// When an external login provider registers an error with during the OAuth process, /// this middleware will detect that, store the errors into cookie data and redirect to the back office login so we can read the errors back out. /// - public class BackOfficeExternalLoginProviderErrorMiddleware : IMiddleware, IConfigureOptions + public class BackOfficeExternalLoginProviderErrorMiddleware : IMiddleware { - /// - /// Adds ourselves to the Umbraco middleware pipeline before any endpoint routes are declared - /// - /// - public void Configure(UmbracoPipelineOptions options) - => options.AddFilter(new UmbracoPipelineFilter( - nameof(BackOfficeExternalLoginProviderErrorMiddleware), - null, - app => app.UseMiddleware(), - null)); - public async Task InvokeAsync(HttpContext context, RequestDelegate next) { var shortCircuit = false; diff --git a/src/Umbraco.Web.BackOffice/Middleware/ConfigureGlobalOptionsForKeepAliveMiddlware.cs b/src/Umbraco.Web.BackOffice/Middleware/ConfigureGlobalOptionsForKeepAliveMiddlware.cs new file mode 100644 index 000000000000..d08e1d7d1f89 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Middleware/ConfigureGlobalOptionsForKeepAliveMiddlware.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Cms.Web.BackOffice.Middleware +{ + /// + /// Ensures the Keep Alive middleware is part of + /// + public sealed class ConfigureGlobalOptionsForKeepAliveMiddlware : IPostConfigureOptions + { + private readonly IOptions _keepAliveSettings; + + public ConfigureGlobalOptionsForKeepAliveMiddlware(IOptions keepAliveSettings) => _keepAliveSettings = keepAliveSettings; + + /// + /// Append the keep alive ping url to the reserved URLs + /// + /// + /// + public void PostConfigure(string name, GlobalSettings options) => options.ReservedUrls += _keepAliveSettings.Value.KeepAlivePingUrl; + } +} diff --git a/src/Umbraco.Web.BackOffice/Middleware/KeepAliveMiddleware.cs b/src/Umbraco.Web.BackOffice/Middleware/KeepAliveMiddleware.cs new file mode 100644 index 000000000000..7c3d8d0905a7 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Middleware/KeepAliveMiddleware.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Web.BackOffice.Middleware +{ + + /// + /// Used for the Umbraco keep alive service. This is terminating middleware. + /// + public class KeepAliveMiddleware : IMiddleware + { + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + if (HttpMethods.IsGet(context.Request.Method) || HttpMethods.IsHead(context.Request.Method)) + { + await context.Response.WriteAsync("I'm alive"); + context.Response.StatusCode = StatusCodes.Status200OK; + } + else + { + context.Response.StatusCode = StatusCodes.Status404NotFound; + } + } + } +} diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilder.cs b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilder.cs new file mode 100644 index 000000000000..34abdf70bd89 --- /dev/null +++ b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilder.cs @@ -0,0 +1,19 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Web.Common.ApplicationBuilder +{ + public interface IUmbracoApplicationBuilder + { + IRuntimeState RuntimeState { get; } + IServiceProvider ApplicationServices { get; } + IApplicationBuilder AppBuilder { get; } + + /// + /// Final call during app building to configure endpoints + /// + /// + void WithEndpoints(Action configureUmbraco); + } +} diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoEndpointBuilder.cs b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoEndpointBuilder.cs index 657a4d42e6e5..4db74dea7504 100644 --- a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoEndpointBuilder.cs +++ b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoEndpointBuilder.cs @@ -5,6 +5,7 @@ namespace Umbraco.Cms.Web.Common.ApplicationBuilder { + /// /// A builder to allow encapsulating the enabled routing features in Umbraco /// diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs new file mode 100644 index 000000000000..2bef61bbab0a --- /dev/null +++ b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Web.Common.ApplicationBuilder +{ + /// + /// A builder to allow encapsulating the enabled endpoints in Umbraco + /// + internal class UmbracoApplicationBuilder : IUmbracoApplicationBuilder + { + public UmbracoApplicationBuilder(IServiceProvider services, IRuntimeState runtimeState, IApplicationBuilder appBuilder) + { + ApplicationServices = services; + RuntimeState = runtimeState; + AppBuilder = appBuilder; + } + + public IServiceProvider ApplicationServices { get; } + public IRuntimeState RuntimeState { get; } + public IApplicationBuilder AppBuilder { get; } + + public void WithEndpoints(Action configureUmbraco) + => AppBuilder.UseEndpoints(endpoints => + { + var umbAppBuilder = (IUmbracoEndpointBuilder)ActivatorUtilities.CreateInstance( + ApplicationServices, + new object[] { AppBuilder, endpoints }); + configureUmbraco(umbAppBuilder); + }); + } +} diff --git a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs index 55e02b8aca3b..1195f7dbac03 100644 --- a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs @@ -25,7 +25,7 @@ public static class ApplicationBuilderExtensions /// /// Configures and use services required for using Umbraco /// - public static IApplicationBuilder UseUmbraco(this IApplicationBuilder app, Action configureUmbraco) + public static IUmbracoApplicationBuilder UseUmbraco(this IApplicationBuilder app) { IOptions startupOptions = app.ApplicationServices.GetRequiredService>(); @@ -75,18 +75,9 @@ public static IApplicationBuilder UseUmbraco(this IApplicationBuilder app, Actio app.RunPostPipeline(startupOptions.Value); app.RunPreEndpointsPipeline(startupOptions.Value); - // create our custom builder and execute the callback - // which will allow executing all IUmbracoApplicationBuilder ext methods - // to create endpoints. - app.UseEndpoints(endpoints => - { - var umbAppBuilder = (IUmbracoEndpointBuilder)ActivatorUtilities.CreateInstance( - app.ApplicationServices, - new object[] { app, endpoints }); - configureUmbraco(umbAppBuilder); - }); - - return app; + return ActivatorUtilities.CreateInstance( + app.ApplicationServices, + new object[] { app }); } private static void RunPrePipeline(this IApplicationBuilder app, UmbracoPipelineOptions startupOptions) diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index 3eb29209a38c..d12d7969fdf4 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -43,7 +43,7 @@ public void ConfigureServices(IServiceCollection services) { #pragma warning disable IDE0022 // Use expression body for methods services.AddUmbraco(_env, _config) - .AddBackOffice() + .AddBackOffice() .AddWebsite() .AddComposers() .Build(); @@ -61,12 +61,15 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseDeveloperExceptionPage(); } - app.UseUmbraco(u => - { - u.UseInstallerEndpoints(); - u.UseBackOfficeEndpoints(); - u.UseWebsiteEndpoints(); - }); + app.UseUmbraco() + .WithBackOffice() + .WithWebsite() + .WithEndpoints(u => + { + u.UseInstallerEndpoints(); + u.UseBackOfficeEndpoints(); + u.UseWebsiteEndpoints(); + }); } } } diff --git a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs index 6f7a119c1bb6..69618004c789 100644 --- a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs @@ -47,9 +47,7 @@ public static IUmbracoBuilder AddWebsite(this IUmbracoBuilder builder) builder.Services.AddSingleton(); - builder.Services - .AddSingleton() - .ConfigureOptions(); + builder.Services.AddSingleton(); builder .AddDistributedCache() diff --git a/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs b/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs index ac40cd7f4b32..54c4ab418660 100644 --- a/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs +++ b/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Web.Common.ApplicationBuilder; +using Umbraco.Cms.Web.Website.Middleware; using Umbraco.Cms.Web.Website.Routing; namespace Umbraco.Extensions @@ -11,26 +12,37 @@ namespace Umbraco.Extensions /// public static partial class UmbracoApplicationBuilderExtensions { + /// + /// Adds all required middleware to run the website + /// + /// + /// + public static IUmbracoApplicationBuilder WithWebsite(this IUmbracoApplicationBuilder builder) + { + builder.AppBuilder.UseMiddleware(); + return builder; + } + /// /// Sets up routes for the front-end umbraco website /// - public static IUmbracoEndpointBuilder UseWebsiteEndpoints(this IUmbracoEndpointBuilder app) + public static IUmbracoEndpointBuilder UseWebsiteEndpoints(this IUmbracoEndpointBuilder builder) { - if (app == null) + if (builder == null) { - throw new ArgumentNullException(nameof(app)); + throw new ArgumentNullException(nameof(builder)); } - if (!app.RuntimeState.UmbracoCanBoot()) + if (!builder.RuntimeState.UmbracoCanBoot()) { - return app; + return builder; } - FrontEndRoutes surfaceRoutes = app.ApplicationServices.GetRequiredService(); - surfaceRoutes.CreateRoutes(app.EndpointRouteBuilder); - app.EndpointRouteBuilder.MapDynamicControllerRoute("/{**slug}"); + FrontEndRoutes surfaceRoutes = builder.ApplicationServices.GetRequiredService(); + surfaceRoutes.CreateRoutes(builder.EndpointRouteBuilder); + builder.EndpointRouteBuilder.MapDynamicControllerRoute("/{**slug}"); - return app; + return builder; } } } diff --git a/src/Umbraco.Web.Website/Middleware/PublicAccessMiddleware.cs b/src/Umbraco.Web.Website/Middleware/PublicAccessMiddleware.cs index 705e9a581f7d..cdf721cfbc20 100644 --- a/src/Umbraco.Web.Website/Middleware/PublicAccessMiddleware.cs +++ b/src/Umbraco.Web.Website/Middleware/PublicAccessMiddleware.cs @@ -1,12 +1,7 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; @@ -14,13 +9,12 @@ using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; -using Umbraco.Cms.Web.Common.ApplicationBuilder; using Umbraco.Cms.Web.Common.Routing; using Umbraco.Cms.Web.Website.Routing; namespace Umbraco.Cms.Web.Website.Middleware { - public class PublicAccessMiddleware : IMiddleware, IConfigureOptions + public class PublicAccessMiddleware : IMiddleware { private readonly ILogger _logger; private readonly IPublicAccessService _publicAccessService; @@ -45,17 +39,6 @@ public PublicAccessMiddleware( _publishedRouter = publishedRouter; } - /// - /// Adds ourselves to the Umbraco middleware pipeline before any endpoint routes are declared - /// - /// - public void Configure(UmbracoPipelineOptions options) - => options.AddFilter(new UmbracoPipelineFilter( - nameof(PublicAccessMiddleware), - null, - app => app.UseMiddleware(), - null)); - public async Task InvokeAsync(HttpContext context, RequestDelegate next) { UmbracoRouteValues umbracoRouteValues = context.Features.Get(); @@ -86,7 +69,7 @@ private async Task EnsurePublishedContentAccess(HttpContext httpContext, Umbraco IPublishedContent publishedContent = routeValues.PublishedRequest?.PublishedContent; if (publishedContent == null) { - throw new InvalidOperationException("There is no PublishedContent."); + return; } var path = publishedContent.Path; From eba861616581ba417020b74b84eb952470e99506 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 19 Apr 2021 18:21:35 +1000 Subject: [PATCH 15/26] adds note --- src/Umbraco.Core/Composing/ICoreComposer.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Composing/ICoreComposer.cs b/src/Umbraco.Core/Composing/ICoreComposer.cs index 24daae75b1ba..c6d0b3510c98 100644 --- a/src/Umbraco.Core/Composing/ICoreComposer.cs +++ b/src/Umbraco.Core/Composing/ICoreComposer.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing { /// /// Represents a core . @@ -7,5 +7,7 @@ /// Core composers compose after the initial composer, and before user composers. /// public interface ICoreComposer : IComposer - { } + { + // TODO: This should die, there should be exactly zero core composers. + } } From 70b1513bb25e42cbd0ac9e1328e06e17b3b23c2c Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 19 Apr 2021 18:50:38 +1000 Subject: [PATCH 16/26] removes unused filter, fixes build --- .../UmbracoTestServerTestBase.cs | 13 +- .../OnlyLocalRequestsAttributeTests.cs | 128 ------------------ 2 files changed, 8 insertions(+), 133 deletions(-) delete mode 100644 src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Filters/OnlyLocalRequestsAttributeTests.cs diff --git a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index 9b0b0b15245f..39afb391aab2 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -180,11 +180,14 @@ public override void ConfigureServices(IServiceCollection services) public override void Configure(IApplicationBuilder app) { - app.UseUmbraco(u => - { - u.UseBackOfficeEndpoints(); - u.UseWebsiteEndpoints(); - }); + app.UseUmbraco() + .WithBackOffice() + .WithWebsite() + .WithEndpoints(u => + { + u.UseBackOfficeEndpoints(); + u.UseWebsiteEndpoints(); + }); } } } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Filters/OnlyLocalRequestsAttributeTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Filters/OnlyLocalRequestsAttributeTests.cs deleted file mode 100644 index 3b129470341e..000000000000 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Filters/OnlyLocalRequestsAttributeTests.cs +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. - -using System.Collections.Generic; -using System.Net; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Routing; -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Web.BackOffice.Filters; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Filters -{ - [TestFixture] - public class OnlyLocalRequestsAttributeTests - { - [Test] - public void Does_Not_Set_Result_When_No_Remote_Address() - { - // Arrange - ActionExecutingContext context = CreateContext(); - var attribute = new OnlyLocalRequestsAttribute(); - - // Act - attribute.OnActionExecuting(context); - - // Assert - Assert.IsNull(context.Result); - } - - [Test] - public void Does_Not_Set_Result_When_Remote_Address_Is_Null_Ip_Address() - { - // Arrange - ActionExecutingContext context = CreateContext(remoteIpAddress: "::1"); - var attribute = new OnlyLocalRequestsAttribute(); - - // Act - attribute.OnActionExecuting(context); - - // Assert - Assert.IsNull(context.Result); - } - - [Test] - public void Does_Not_Set_Result_When_Remote_Address_Matches_Local_Address() - { - // Arrange - ActionExecutingContext context = CreateContext(remoteIpAddress: "100.1.2.3", localIpAddress: "100.1.2.3"); - var attribute = new OnlyLocalRequestsAttribute(); - - // Act - attribute.OnActionExecuting(context); - - // Assert - Assert.IsNull(context.Result); - } - - [Test] - public void Returns_Not_Found_When_Remote_Address_Does_Not_Match_Local_Address() - { - // Arrange - ActionExecutingContext context = CreateContext(remoteIpAddress: "100.1.2.3", localIpAddress: "100.1.2.2"); - var attribute = new OnlyLocalRequestsAttribute(); - - // Act - attribute.OnActionExecuting(context); - - // Assert - var typedResult = context.Result as NotFoundResult; - Assert.IsNotNull(typedResult); - } - - [Test] - public void Does_Not_Set_Result_When_Remote_Address_Matches_LoopBack_Address() - { - // Arrange - ActionExecutingContext context = CreateContext(remoteIpAddress: "127.0.0.1", localIpAddress: "::1"); - var attribute = new OnlyLocalRequestsAttribute(); - - // Act - attribute.OnActionExecuting(context); - - // Assert - Assert.IsNull(context.Result); - } - - [Test] - public void Returns_Not_Found_When_Remote_Address_Does_Not_Match_LoopBack_Address() - { - // Arrange - ActionExecutingContext context = CreateContext(remoteIpAddress: "100.1.2.3", localIpAddress: "::1"); - var attribute = new OnlyLocalRequestsAttribute(); - - // Act - attribute.OnActionExecuting(context); - - // Assert - var typedResult = context.Result as NotFoundResult; - Assert.IsNotNull(typedResult); - } - - private static ActionExecutingContext CreateContext(string remoteIpAddress = null, string localIpAddress = null) - { - var httpContext = new DefaultHttpContext(); - if (!string.IsNullOrEmpty(remoteIpAddress)) - { - httpContext.Connection.RemoteIpAddress = IPAddress.Parse(remoteIpAddress); - } - - if (!string.IsNullOrEmpty(localIpAddress)) - { - httpContext.Connection.LocalIpAddress = IPAddress.Parse(localIpAddress); - } - - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - - return new ActionExecutingContext( - actionContext, - new List(), - new Dictionary(), - new Mock().Object); - } - } -} From fe7975d54c3eb5185d00ad2ea32e9a21ca1384cc Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 19 Apr 2021 20:56:46 +1000 Subject: [PATCH 17/26] fixes WebPath and tests --- src/Umbraco.Core/Routing/WebPath.cs | 49 ++++++++++++++----- .../Umbraco.Core/Routing/WebPathTests.cs | 5 +- .../HostedServices/KeepAliveTests.cs | 8 +-- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Core/Routing/WebPath.cs b/src/Umbraco.Core/Routing/WebPath.cs index acc5e323c179..d94a7b94a522 100644 --- a/src/Umbraco.Core/Routing/WebPath.cs +++ b/src/Umbraco.Core/Routing/WebPath.cs @@ -1,32 +1,55 @@ using System; -using System.Linq; -using Umbraco.Extensions; +using System.Text; namespace Umbraco.Cms.Core.Routing { public class WebPath { + private const char Separator = '/'; + public static string Combine(params string[] paths) { - const string separator = "/"; - - if (paths == null) throw new ArgumentNullException(nameof(paths)); - if (!paths.Any()) return string.Empty; - - var result = paths[0].TrimEnd(separator); + if (paths == null) + { + throw new ArgumentNullException(nameof(paths)); + } - if(!(result.StartsWith(separator) || result.StartsWith("~" + separator))) + if (paths.Length == 0) { - result = separator + result; + return string.Empty; } - for (var index = 1; index < paths.Length; index++) + var sb = new StringBuilder(); + + for (var index = 0; index < paths.Length; index++) { + var path = paths[index]; + var start = 0; + var count = path.Length; + var isFirst = index == 0; + var isLast = index == paths.Length - 1; + + // don't trim start if it's the first + if (!isFirst && path[0] == Separator) + { + start = 1; + } + + // always trim end + if (path[path.Length - 1] == Separator) + { + count = path.Length - 1; + } + + sb.Append(path, start, count - start); - result +=separator + paths[index].Trim(separator); + if (!isLast) + { + sb.Append(Separator); + } } - return result; + return sb.ToString(); } } } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/WebPathTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/WebPathTests.cs index 2f9c05994fc0..a3e3da506067 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/WebPathTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/WebPathTests.cs @@ -15,13 +15,14 @@ public class WebPathTests [TestCase("/umbraco", "/config", "/lang", ExpectedResult = "/umbraco/config/lang")] [TestCase("/umbraco/", "/config/", "/lang/", ExpectedResult = "/umbraco/config/lang")] [TestCase("/umbraco/", "config/", "lang/", ExpectedResult = "/umbraco/config/lang")] - [TestCase("umbraco", "config", "lang", ExpectedResult = "/umbraco/config/lang")] - [TestCase("umbraco", ExpectedResult = "/umbraco")] + [TestCase("umbraco", "config", "lang", ExpectedResult = "umbraco/config/lang")] + [TestCase("umbraco", ExpectedResult = "umbraco")] [TestCase("~/umbraco", "config", "lang", ExpectedResult = "~/umbraco/config/lang")] [TestCase("~/umbraco", "/config", "/lang", ExpectedResult = "~/umbraco/config/lang")] [TestCase("~/umbraco/", "/config/", "/lang/", ExpectedResult = "~/umbraco/config/lang")] [TestCase("~/umbraco/", "config/", "lang/", ExpectedResult = "~/umbraco/config/lang")] [TestCase("~/umbraco", ExpectedResult = "~/umbraco")] + [TestCase("https://hello.com/", "/world", ExpectedResult = "https://hello.com/world")] public string Combine(params string[] parts) => WebPath.Combine(parts); [Test] diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/KeepAliveTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/KeepAliveTests.cs index 4266ab22b4e2..557e87fb0c9d 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/KeepAliveTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/KeepAliveTests.cs @@ -78,8 +78,10 @@ private KeepAlive CreateKeepAlive( DisableKeepAliveTask = !enabled, }; - var mockRequestAccessor = new Mock(); - mockRequestAccessor.SetupGet(x => x.ApplicationMainUrl).Returns(new Uri(ApplicationUrl)); + var mockHostingEnvironment = new Mock(); + mockHostingEnvironment.SetupGet(x => x.ApplicationMainUrl).Returns(new Uri(ApplicationUrl)); + mockHostingEnvironment.Setup(x => x.ToAbsolute(It.IsAny())) + .Returns((string s) => s.TrimStart('~')); var mockServerRegistrar = new Mock(); mockServerRegistrar.Setup(x => x.CurrentServerRole).Returns(serverRole); @@ -103,7 +105,7 @@ private KeepAlive CreateKeepAlive( mockHttpClientFactory.Setup(x => x.CreateClient(It.IsAny())).Returns(httpClient); return new KeepAlive( - mockRequestAccessor.Object, + mockHostingEnvironment.Object, mockMainDom.Object, Options.Create(settings), mockLogger.Object, From fd86c985ea7ee776a2b76ea69dd14d037914c55d Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 19 Apr 2021 21:02:26 +1000 Subject: [PATCH 18/26] Looks up entities in one query --- .../Controllers/PublicAccessController.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/PublicAccessController.cs b/src/Umbraco.Web.BackOffice/Controllers/PublicAccessController.cs index 3b347c3d7636..15859660f760 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PublicAccessController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PublicAccessController.cs @@ -62,8 +62,19 @@ public ActionResult GetPublicAccess(int contentId) return Ok(); } - IEntitySlim loginPageEntity = _entityService.Get(entry.LoginNodeId, UmbracoObjectTypes.Document); - IEntitySlim errorPageEntity = _entityService.Get(entry.NoAccessNodeId, UmbracoObjectTypes.Document); + var nodes = _entityService + .GetAll(UmbracoObjectTypes.Document, entry.LoginNodeId, entry.NoAccessNodeId) + .ToDictionary(x => x.Id); + + if (!nodes.TryGetValue(entry.LoginNodeId, out IEntitySlim loginPageEntity)) + { + throw new InvalidOperationException($"Login node with id ${entry.LoginNodeId} was not found"); + } + + if (!nodes.TryGetValue(entry.NoAccessNodeId, out IEntitySlim errorPageEntity)) + { + throw new InvalidOperationException($"Error node with id ${entry.LoginNodeId} was not found"); + } // unwrap the current public access setup for the client // - this API method is the single point of entry for both "modes" of public access (single user and role based) From 5678256171acd54cf74825a91b43e7c4186d6333 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 19 Apr 2021 21:03:27 +1000 Subject: [PATCH 19/26] remove usings --- .../UmbracoBuilder.BackOfficeIdentity.cs | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs index f9b0654096b2..dfc2423d6d46 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs @@ -1,32 +1,12 @@ using System; -using System.Linq; -using Ganss.XSS; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Net; using Umbraco.Cms.Core.Security; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Infrastructure.DependencyInjection; -using Umbraco.Cms.Infrastructure.WebAssets; -using Umbraco.Cms.Web.BackOffice.Controllers; -using Umbraco.Cms.Web.BackOffice.Filters; -using Umbraco.Cms.Web.BackOffice.Middleware; -using Umbraco.Cms.Web.BackOffice.ModelsBuilder; -using Umbraco.Cms.Web.BackOffice.Routing; using Umbraco.Cms.Web.BackOffice.Security; -using Umbraco.Cms.Web.BackOffice.Services; -using Umbraco.Cms.Web.BackOffice.SignalR; -using Umbraco.Cms.Web.BackOffice.Trees; using Umbraco.Cms.Web.Common.AspNetCore; -using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Cms.Web.Common.Security; namespace Umbraco.Extensions From bafa2ace1dd698a3e835c1ee7e2c7548e6cc5fbb Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 19 Apr 2021 21:13:28 +1000 Subject: [PATCH 20/26] Fix test, remove stylesheet --- src/Umbraco.Core/Routing/WebPath.cs | 8 ++++---- .../Builders/StylesheetBuilderTests.cs | 2 +- .../DependencyInjection/UmbracoBuilderExtensions.cs | 9 --------- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Core/Routing/WebPath.cs b/src/Umbraco.Core/Routing/WebPath.cs index d94a7b94a522..91313c827808 100644 --- a/src/Umbraco.Core/Routing/WebPath.cs +++ b/src/Umbraco.Core/Routing/WebPath.cs @@ -5,7 +5,7 @@ namespace Umbraco.Cms.Core.Routing { public class WebPath { - private const char Separator = '/'; + public const char PathSeparator = '/'; public static string Combine(params string[] paths) { @@ -30,13 +30,13 @@ public static string Combine(params string[] paths) var isLast = index == paths.Length - 1; // don't trim start if it's the first - if (!isFirst && path[0] == Separator) + if (!isFirst && path[0] == PathSeparator) { start = 1; } // always trim end - if (path[path.Length - 1] == Separator) + if (path[path.Length - 1] == PathSeparator) { count = path.Length - 1; } @@ -45,7 +45,7 @@ public static string Combine(params string[] paths) if (!isLast) { - sb.Append(Separator); + sb.Append(PathSeparator); } } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/StylesheetBuilderTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/StylesheetBuilderTests.cs index 51929cc445cf..4b7c58c1045f 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/StylesheetBuilderTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/StylesheetBuilderTests.cs @@ -16,7 +16,7 @@ public class StylesheetBuilderTests public void Is_Built_Correctly() { // Arrange - var testPath = WebPath.Combine("css", "styles.css"); + var testPath = WebPath.PathSeparator + WebPath.Combine("css", "styles.css"); const string testContent = @"body { color:#000; } .bold {font-weight:bold;}"; var builder = new StylesheetBuilder(); diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index 63f04137da7e..e8eea703a0ba 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -1,18 +1,12 @@ -using System; using System.Linq; using Ganss.XSS; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; -using Umbraco.Cms.Core.Net; -using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.DependencyInjection; using Umbraco.Cms.Infrastructure.WebAssets; @@ -25,9 +19,6 @@ using Umbraco.Cms.Web.BackOffice.Services; using Umbraco.Cms.Web.BackOffice.SignalR; using Umbraco.Cms.Web.BackOffice.Trees; -using Umbraco.Cms.Web.Common.AspNetCore; -using Umbraco.Cms.Web.Common.Authorization; -using Umbraco.Cms.Web.Common.Security; namespace Umbraco.Extensions { From 821a6994c035e8c63d887cd07d97a7305c9dcd01 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 19 Apr 2021 13:28:35 +0200 Subject: [PATCH 21/26] Set status code before we write to response to avoid error --- .../Middleware/KeepAliveMiddleware.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Middleware/KeepAliveMiddleware.cs b/src/Umbraco.Web.BackOffice/Middleware/KeepAliveMiddleware.cs index 7c3d8d0905a7..733b1699aad9 100644 --- a/src/Umbraco.Web.BackOffice/Middleware/KeepAliveMiddleware.cs +++ b/src/Umbraco.Web.BackOffice/Middleware/KeepAliveMiddleware.cs @@ -1,6 +1,5 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.Middleware { @@ -14,13 +13,14 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next) { if (HttpMethods.IsGet(context.Request.Method) || HttpMethods.IsHead(context.Request.Method)) { - await context.Response.WriteAsync("I'm alive"); context.Response.StatusCode = StatusCodes.Status200OK; + await context.Response.WriteAsync("I'm alive"); + } else { context.Response.StatusCode = StatusCodes.Status404NotFound; - } + } } } } From f98a99c18ca81f675d6e9b34a2d97888edf67832 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 19 Apr 2021 23:13:53 +1000 Subject: [PATCH 22/26] Ensures that users and members are validated when logging in. Shares more code between users and members. --- .../Security/BackOfficeIdentityBuilder.cs | 2 +- .../Security/BackOfficeIdentityUser.cs | 40 +----------------- .../Security/MemberIdentityOptions.cs | 11 ----- .../Security/MemberIdentityUser.cs | 41 +------------------ .../Security/UmbracoIdentityUser.cs | 38 +++++++++++++++++ .../Security/UmbracoUserConfirmation.cs | 15 +++++++ .../Security/MemberManagerTests.cs | 6 +-- .../Security/MemberSignInManagerTests.cs | 4 +- .../ConfigureBackOfficeIdentityOptions.cs | 23 +++++------ .../UmbracoBuilder.MembersIdentity.cs | 6 ++- .../PasswordConfigurationExtensions.cs | 17 ++++++++ .../ConfigureMemberIdentityOptions.cs | 35 ++++++++++++++++ .../Security/MemberManager.cs | 2 +- .../Security/UmbracoSignInManager.cs | 12 +++++- 14 files changed, 141 insertions(+), 111 deletions(-) delete mode 100644 src/Umbraco.Infrastructure/Security/MemberIdentityOptions.cs create mode 100644 src/Umbraco.Infrastructure/Security/UmbracoUserConfirmation.cs create mode 100644 src/Umbraco.Web.Common/Extensions/PasswordConfigurationExtensions.cs create mode 100644 src/Umbraco.Web.Common/Security/ConfigureMemberIdentityOptions.cs diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityBuilder.cs b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityBuilder.cs index 9b1f581b8281..088afc714924 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityBuilder.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityBuilder.cs @@ -41,7 +41,7 @@ private void InitializeServices(IServiceCollection services) services => new BackOfficePasswordHasher( new LegacyPasswordSecurity(), services.GetRequiredService())); - services.TryAddScoped, DefaultUserConfirmation>(); + services.TryAddScoped, UmbracoUserConfirmation>(); } // override to add itself, by default identity only wants a single IdentityErrorDescriber diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs index 68aae8340abd..0ca109b741fe 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs @@ -13,8 +13,6 @@ namespace Umbraco.Cms.Core.Security /// public class BackOfficeIdentityUser : UmbracoIdentityUser { - private string _name; - private string _passwordConfig; private string _culture; private IReadOnlyCollection _groups; private string[] _allowedSections; @@ -54,7 +52,7 @@ public static BackOfficeIdentityUser CreateNew(GlobalSettings globalSettings, st user.Id = null; user.HasIdentity = false; user._culture = culture; - user._name = name; + user.Name = name; user.EnableChangeTracking(); return user; } @@ -83,25 +81,6 @@ public BackOfficeIdentityUser(GlobalSettings globalSettings, int userId, IEnumer public int[] CalculatedMediaStartNodeIds { get; set; } public int[] CalculatedContentStartNodeIds { get; set; } - /// - /// Gets or sets the user's real name - /// - public string Name - { - get => _name; - set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); - } - - /// - /// Gets or sets the password config - /// - public string PasswordConfig - { - get => _passwordConfig; - set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfig)); - } - - /// /// Gets or sets content start nodes assigned to the User (not ones assigned to the user's groups) /// @@ -180,23 +159,6 @@ public IReadOnlyCollection Groups } } - /// - /// Gets a value indicating whether the user is locked out based on the user's lockout end date - /// - public bool IsLockedOut - { - get - { - bool isLocked = LockoutEnd.HasValue && LockoutEnd.Value.ToLocalTime() >= DateTime.Now; - return isLocked; - } - } - - /// - /// Gets or sets a value indicating whether the IUser IsApproved - /// - public bool IsApproved { get; set; } - private static string UserIdToString(int userId) => string.Intern(userId.ToString()); } } diff --git a/src/Umbraco.Infrastructure/Security/MemberIdentityOptions.cs b/src/Umbraco.Infrastructure/Security/MemberIdentityOptions.cs deleted file mode 100644 index 4e05797a044e..000000000000 --- a/src/Umbraco.Infrastructure/Security/MemberIdentityOptions.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.AspNetCore.Identity; - -namespace Umbraco.Cms.Core.Security -{ - /// - /// Identity options specifically for the Umbraco members identity implementation - /// - public class MemberIdentityOptions : IdentityOptions - { - } -} diff --git a/src/Umbraco.Infrastructure/Security/MemberIdentityUser.cs b/src/Umbraco.Infrastructure/Security/MemberIdentityUser.cs index 22b4e2645bdb..3cfe779d1078 100644 --- a/src/Umbraco.Infrastructure/Security/MemberIdentityUser.cs +++ b/src/Umbraco.Infrastructure/Security/MemberIdentityUser.cs @@ -12,9 +12,7 @@ namespace Umbraco.Cms.Core.Security /// public class MemberIdentityUser : UmbracoIdentityUser { - private string _name; - private string _comments; - private string _passwordConfig; + private string _comments; private IReadOnlyCollection _groups; // Custom comparer for enumerables @@ -52,20 +50,11 @@ public static MemberIdentityUser CreateNew(string username, string email, string user.MemberTypeAlias = memberTypeAlias; user.Id = null; user.HasIdentity = false; - user._name = name; + user.Name = name; user.EnableChangeTracking(); return user; } - /// - /// Gets or sets the member's real name - /// - public string Name - { - get => _name; - set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); - } - /// /// Gets or sets the member's comments /// @@ -84,15 +73,6 @@ public string Comments // No change tracking because the persisted value is readonly public Guid Key { get; set; } - /// - /// Gets or sets the password config - /// - public string PasswordConfig - { - get => _passwordConfig; - set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfig)); - } - /// /// Gets or sets the user groups /// @@ -120,23 +100,6 @@ public IReadOnlyCollection Groups } } - /// - /// Gets a value indicating whether the member is locked out - /// - public bool IsLockedOut - { - get - { - bool isLocked = LockoutEnd.HasValue && LockoutEnd.Value.ToLocalTime() >= DateTime.Now; - return isLocked; - } - } - - /// - /// Gets or sets a value indicating whether the member is approved - /// - public bool IsApproved { get; set; } - /// /// Gets or sets the alias of the member type /// diff --git a/src/Umbraco.Infrastructure/Security/UmbracoIdentityUser.cs b/src/Umbraco.Infrastructure/Security/UmbracoIdentityUser.cs index 978f7650d320..f91fac1549ed 100644 --- a/src/Umbraco.Infrastructure/Security/UmbracoIdentityUser.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoIdentityUser.cs @@ -29,6 +29,8 @@ namespace Umbraco.Cms.Core.Security /// public abstract class UmbracoIdentityUser : IdentityUser, IRememberBeingDirty { + private string _name; + private string _passwordConfig; private string _id; private string _email; private string _userName; @@ -247,6 +249,42 @@ public override string UserName /// protected BeingDirty BeingDirty { get; } = new BeingDirty(); + /// + /// Gets a value indicating whether the user is locked out based on the user's lockout end date + /// + public bool IsLockedOut + { + get + { + bool isLocked = LockoutEnabled && LockoutEnd.HasValue && LockoutEnd.Value.ToLocalTime() >= DateTime.Now; + return isLocked; + } + } + + /// + /// Gets or sets a value indicating whether the IUser IsApproved + /// + public bool IsApproved { get; set; } + + /// + /// Gets or sets the user's real name + /// + public string Name + { + get => _name; + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); + } + + /// + /// Gets or sets the password config + /// + public string PasswordConfig + { + // TODO: Implement this for members: AB#11550 + get => _passwordConfig; + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfig)); + } + /// public bool IsDirty() => BeingDirty.IsDirty(); diff --git a/src/Umbraco.Infrastructure/Security/UmbracoUserConfirmation.cs b/src/Umbraco.Infrastructure/Security/UmbracoUserConfirmation.cs new file mode 100644 index 000000000000..04166e3c0d4d --- /dev/null +++ b/src/Umbraco.Infrastructure/Security/UmbracoUserConfirmation.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; + +namespace Umbraco.Cms.Core.Security +{ + /// + /// Confirms whether a user is approved or not + /// + public class UmbracoUserConfirmation : DefaultUserConfirmation + where TUser: UmbracoIdentityUser + { + public override Task IsConfirmedAsync(UserManager manager, TUser user) + => Task.FromResult(user.IsApproved); + } +} diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs index 7a345e8edab1..c09222bd6fc6 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs @@ -26,7 +26,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public class MemberManagerTests { private MemberUserStore _fakeMemberStore; - private Mock> _mockIdentityOptions; + private Mock> _mockIdentityOptions; private Mock> _mockPasswordHasher; private Mock _mockMemberService; private Mock _mockServiceProviders; @@ -41,8 +41,8 @@ public MemberManager CreateSut() new Mock().Object, new IdentityErrorDescriber()); - _mockIdentityOptions = new Mock>(); - var idOptions = new MemberIdentityOptions { Lockout = { AllowedForNewUsers = false } }; + _mockIdentityOptions = new Mock>(); + var idOptions = new IdentityOptions { Lockout = { AllowedForNewUsers = false } }; _mockIdentityOptions.Setup(o => o.Value).Returns(idOptions); _mockPasswordHasher = new Mock>(); 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 f33344e49b21..752f4783ad8f 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs @@ -26,7 +26,7 @@ public class MemberSignInManagerTests private readonly Mock _memberManager = MockMemberManager(); public UserClaimsPrincipalFactory CreateClaimsFactory(MemberManager userMgr) - => new UserClaimsPrincipalFactory(userMgr, Options.Create(new MemberIdentityOptions())); + => new UserClaimsPrincipalFactory(userMgr, Options.Create(new IdentityOptions())); public MemberSignInManager CreateSut() { @@ -61,7 +61,7 @@ private static Mock MockMemberManager() => new Mock( Mock.Of(), Mock.Of>(), - Options.Create(new MemberIdentityOptions()), + Options.Create(new IdentityOptions()), Mock.Of>(), Enumerable.Empty>(), Enumerable.Empty>(), diff --git a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeIdentityOptions.cs b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeIdentityOptions.cs index 842e6c728933..8ffad24d544d 100644 --- a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeIdentityOptions.cs +++ b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeIdentityOptions.cs @@ -1,10 +1,9 @@ using System; using System.Security.Claims; -using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; -using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Security; +using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Web.BackOffice.Security @@ -12,7 +11,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security /// /// Used to configure for the Umbraco Back office /// - public class ConfigureBackOfficeIdentityOptions : IConfigureOptions + public sealed class ConfigureBackOfficeIdentityOptions : IConfigureOptions { private readonly UserPasswordConfigurationSettings _userPasswordConfiguration; @@ -23,26 +22,26 @@ public ConfigureBackOfficeIdentityOptions(IOptions() .AddMemberManager() .AddSignInManager() - .AddErrorDescriber(); + .AddErrorDescriber() + .AddUserConfirmation>(); + + services.ConfigureOptions(); services.ConfigureApplicationCookie(x => { diff --git a/src/Umbraco.Web.Common/Extensions/PasswordConfigurationExtensions.cs b/src/Umbraco.Web.Common/Extensions/PasswordConfigurationExtensions.cs new file mode 100644 index 000000000000..abe0a997303f --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/PasswordConfigurationExtensions.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Identity; +using Umbraco.Cms.Core.Configuration; + +namespace Umbraco.Extensions +{ + public static class PasswordConfigurationExtensions + { + public static void ConfigurePasswordOptions(this PasswordOptions output, IPasswordConfiguration input) + { + output.RequiredLength = input.RequiredLength; + output.RequireNonAlphanumeric = input.RequireNonLetterOrDigit; + output.RequireDigit = input.RequireDigit; + output.RequireLowercase = input.RequireLowercase; + output.RequireUppercase = input.RequireUppercase; + } + } +} diff --git a/src/Umbraco.Web.Common/Security/ConfigureMemberIdentityOptions.cs b/src/Umbraco.Web.Common/Security/ConfigureMemberIdentityOptions.cs new file mode 100644 index 000000000000..3abe5f042898 --- /dev/null +++ b/src/Umbraco.Web.Common/Security/ConfigureMemberIdentityOptions.cs @@ -0,0 +1,35 @@ +using System; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Web.Common.Security +{ + public sealed class ConfigureMemberIdentityOptions : IConfigureOptions + { + private readonly MemberPasswordConfigurationSettings _memberPasswordConfiguration; + + public ConfigureMemberIdentityOptions(IOptions memberPasswordConfiguration) + { + _memberPasswordConfiguration = memberPasswordConfiguration.Value; + } + + public void Configure(IdentityOptions options) + { + options.SignIn.RequireConfirmedAccount = true; // uses our custom IUserConfirmation + options.SignIn.RequireConfirmedEmail = false; // not implemented + options.SignIn.RequireConfirmedPhoneNumber = false; // not implemented + + options.User.RequireUniqueEmail = true; + + options.Lockout.AllowedForNewUsers = true; + // TODO: Implement this + options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromDays(30); + + options.Password.ConfigurePasswordOptions(_memberPasswordConfiguration); + + options.Lockout.MaxFailedAccessAttempts = _memberPasswordConfiguration.MaxFailedAccessAttemptsBeforeLockout; + } + } +} diff --git a/src/Umbraco.Web.Common/Security/MemberManager.cs b/src/Umbraco.Web.Common/Security/MemberManager.cs index 0ea7f2e74c4e..ce09f02b63a2 100644 --- a/src/Umbraco.Web.Common/Security/MemberManager.cs +++ b/src/Umbraco.Web.Common/Security/MemberManager.cs @@ -24,7 +24,7 @@ public class MemberManager : UmbracoUserManager store, - IOptions optionsAccessor, + IOptions optionsAccessor, IPasswordHasher passwordHasher, IEnumerable> userValidators, IEnumerable> passwordValidators, diff --git a/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs b/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs index 061787c6f68f..dd52b397d3a9 100644 --- a/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs +++ b/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs @@ -25,7 +25,15 @@ public abstract class UmbracoSignInManager : SignInManager // 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) + public UmbracoSignInManager( + UserManager userManager, + IHttpContextAccessor contextAccessor, + IUserClaimsPrincipalFactory claimsFactory, + IOptions optionsAccessor, + ILogger> logger, + IAuthenticationSchemeProvider schemes, + IUserConfirmation confirmation) + : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) { } @@ -38,7 +46,7 @@ public UmbracoSignInManager(UserManager userManager, IHttpContextAccessor 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); + SignInResult result = await base.PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure); return await HandleSignIn(user, user.UserName, result); } From 73600dc75e6582f42a4aae127badfa0446ed9386 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 20 Apr 2021 12:38:44 +1000 Subject: [PATCH 23/26] Fixes RepositoryCacheKeys to ensure the keys are normalized --- .../Repositories/RepositoryCacheKeys.cs | 4 +- src/Umbraco.Web.UI.NetCore/Startup.cs | 65 +++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/RepositoryCacheKeys.cs b/src/Umbraco.Core/Persistence/Repositories/RepositoryCacheKeys.cs index f5388d8f953e..5f6a20d29d6c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RepositoryCacheKeys.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RepositoryCacheKeys.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; namespace Umbraco.Cms.Core.Persistence.Repositories @@ -16,6 +16,6 @@ public static string GetKey() return Keys.TryGetValue(type, out var key) ? key : (Keys[type] = "uRepo_" + type.Name + "_"); } - public static string GetKey(object id) => GetKey() + id; + public static string GetKey(object id) => GetKey() + id.ToString().ToUpperInvariant(); } } diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index d12d7969fdf4..ce6f4a8df305 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -6,7 +6,10 @@ using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Infrastructure.DependencyInjection; using Umbraco.Cms.Web.BackOffice.ModelsBuilder; +using Microsoft.Identity.Web; using Umbraco.Extensions; +using Umbraco.Cms.Web.BackOffice.Security; +using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Hosting; namespace Umbraco.Cms.Web.UI.NetCore @@ -44,6 +47,68 @@ public void ConfigureServices(IServiceCollection services) #pragma warning disable IDE0022 // Use expression body for methods services.AddUmbraco(_env, _config) .AddBackOffice() + .AddBackOfficeExternalLogins(logins => + { + var loginProviderOptions = new BackOfficeExternalLoginProviderOptions( + "btn-google", + "fa-google", + new ExternalSignInAutoLinkOptions(true) + { + OnAutoLinking = (user, login) => + { + }, + OnExternalLogin = (user, login) => + { + user.Claims.Add(new IdentityUserClaim + { + ClaimType = "hello", + ClaimValue = "world" + }); + return true; + } + }, + denyLocalLogin: false, + autoRedirectLoginToExternalProvider: false); + + logins.AddBackOfficeLogin( + loginProviderOptions, + auth => + { + auth.AddGoogle( + auth.SchemeForBackOffice("Google"), // The scheme must be set with this method to work for the back office + options => + { + // By default this is '/signin-google' but it needs to be changed to this + options.CallbackPath = "/umbraco-google-signin"; + options.ClientId = "1072120697051-p41pro11srud3o3n90j7m00geq426jqt.apps.googleusercontent.com"; + options.ClientSecret = "cs_LJTXh2rtI01C5OIt9WFkt"; + }); + + // NOTE: Adding additional providers here is possible via the API but + // it will mean that the same BackOfficeExternalLoginProviderOptions will be registered + // for them. In some weird cases maybe people would want that? + }); + + //logins.AddBackOfficeLogin( + // new BackOfficeExternalLoginProviderOptions("btn-microsoft", "fa-windows"), + // auth => + // { + // auth.AddMicrosoftIdentityWebApp( + // options => + // { + // options.SaveTokens = true; + + // // By default this is '/signin-oidc' but it needs to be changed to this + // options.CallbackPath = "/umbraco-signin-oidc"; + // options.Instance = "https://login.microsoftonline.com/"; + // options.TenantId = "3bb0b4c5-364f-4394-ad36-0f29f95e5ddd"; + // options.ClientId = "56e98cad-ed2d-4f1b-8f85-bef11adc163f"; + // options.ClientSecret = "-1E9_fdPHi_ZkSQOb2.O5LG025sv6-NQ3h"; + // }, + // openIdConnectScheme: auth.SchemeForBackOffice("AzureAD"), // The scheme must be set with this method to work for the back office + // cookieScheme: "Fake"); + // }); + }) .AddWebsite() .AddComposers() .Build(); From 774e05adf4926201b174ecd798e3eb82a3f63c60 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 20 Apr 2021 12:39:30 +1000 Subject: [PATCH 24/26] oops didn't mean to commit this --- src/Umbraco.Web.UI.NetCore/Startup.cs | 65 --------------------------- 1 file changed, 65 deletions(-) diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index ce6f4a8df305..d12d7969fdf4 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -6,10 +6,7 @@ using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Infrastructure.DependencyInjection; using Umbraco.Cms.Web.BackOffice.ModelsBuilder; -using Microsoft.Identity.Web; using Umbraco.Extensions; -using Umbraco.Cms.Web.BackOffice.Security; -using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Hosting; namespace Umbraco.Cms.Web.UI.NetCore @@ -47,68 +44,6 @@ public void ConfigureServices(IServiceCollection services) #pragma warning disable IDE0022 // Use expression body for methods services.AddUmbraco(_env, _config) .AddBackOffice() - .AddBackOfficeExternalLogins(logins => - { - var loginProviderOptions = new BackOfficeExternalLoginProviderOptions( - "btn-google", - "fa-google", - new ExternalSignInAutoLinkOptions(true) - { - OnAutoLinking = (user, login) => - { - }, - OnExternalLogin = (user, login) => - { - user.Claims.Add(new IdentityUserClaim - { - ClaimType = "hello", - ClaimValue = "world" - }); - return true; - } - }, - denyLocalLogin: false, - autoRedirectLoginToExternalProvider: false); - - logins.AddBackOfficeLogin( - loginProviderOptions, - auth => - { - auth.AddGoogle( - auth.SchemeForBackOffice("Google"), // The scheme must be set with this method to work for the back office - options => - { - // By default this is '/signin-google' but it needs to be changed to this - options.CallbackPath = "/umbraco-google-signin"; - options.ClientId = "1072120697051-p41pro11srud3o3n90j7m00geq426jqt.apps.googleusercontent.com"; - options.ClientSecret = "cs_LJTXh2rtI01C5OIt9WFkt"; - }); - - // NOTE: Adding additional providers here is possible via the API but - // it will mean that the same BackOfficeExternalLoginProviderOptions will be registered - // for them. In some weird cases maybe people would want that? - }); - - //logins.AddBackOfficeLogin( - // new BackOfficeExternalLoginProviderOptions("btn-microsoft", "fa-windows"), - // auth => - // { - // auth.AddMicrosoftIdentityWebApp( - // options => - // { - // options.SaveTokens = true; - - // // By default this is '/signin-oidc' but it needs to be changed to this - // options.CallbackPath = "/umbraco-signin-oidc"; - // options.Instance = "https://login.microsoftonline.com/"; - // options.TenantId = "3bb0b4c5-364f-4394-ad36-0f29f95e5ddd"; - // options.ClientId = "56e98cad-ed2d-4f1b-8f85-bef11adc163f"; - // options.ClientSecret = "-1E9_fdPHi_ZkSQOb2.O5LG025sv6-NQ3h"; - // }, - // openIdConnectScheme: auth.SchemeForBackOffice("AzureAD"), // The scheme must be set with this method to work for the back office - // cookieScheme: "Fake"); - // }); - }) .AddWebsite() .AddComposers() .Build(); From 8326b0428c4698552d3730ddc1167014ce7d5513 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 20 Apr 2021 14:08:03 +1000 Subject: [PATCH 25/26] Fix casing issues with caching, stop boxing value types for all cache operations, stop re-creating string keys in DefaultRepositoryCachePolicy --- .../Cache/ContentCacheRefresher.cs | 4 +- src/Umbraco.Core/Cache/MacroCacheRefresher.cs | 2 +- src/Umbraco.Core/Cache/MediaCacheRefresher.cs | 4 +- .../Cache/MemberCacheRefresher.cs | 4 +- .../Cache/RelationTypeCacheRefresher.cs | 4 +- src/Umbraco.Core/Cache/UserCacheRefresher.cs | 2 +- .../Cache/UserGroupCacheRefresher.cs | 2 +- .../Repositories/RepositoryCacheKeys.cs | 24 +++++-- .../Cache/DefaultRepositoryCachePolicy.cs | 55 ++++++++++------ .../Implement/ConsentRepository.cs | 2 +- .../Implement/DictionaryRepository.cs | 12 ++-- .../Implement/DocumentRepository.cs | 2 +- .../Repositories/Implement/MediaRepository.cs | 2 +- .../Implement/MemberRepository.cs | 2 +- .../Repositories/Implement/UserRepository.cs | 8 +++ src/Umbraco.Web.UI.NetCore/Startup.cs | 65 +++++++++++++++++++ 16 files changed, 148 insertions(+), 46 deletions(-) diff --git a/src/Umbraco.Core/Cache/ContentCacheRefresher.cs b/src/Umbraco.Core/Cache/ContentCacheRefresher.cs index 26cf00a2d969..2165123b8876 100644 --- a/src/Umbraco.Core/Cache/ContentCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/ContentCacheRefresher.cs @@ -55,9 +55,9 @@ public override void Refresh(JsonPayload[] payloads) foreach (var payload in payloads.Where(x => x.Id != default)) { //By INT Id - isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Id)); + isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Id)); //By GUID Key - isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Key)); + isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Key)); _idKeyMap.ClearCache(payload.Id); diff --git a/src/Umbraco.Core/Cache/MacroCacheRefresher.cs b/src/Umbraco.Core/Cache/MacroCacheRefresher.cs index fa4dca5ecc33..3c1ad46f2cbc 100644 --- a/src/Umbraco.Core/Cache/MacroCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/MacroCacheRefresher.cs @@ -53,7 +53,7 @@ public override void Refresh(string json) var macroRepoCache = AppCaches.IsolatedCaches.Get(); if (macroRepoCache) { - macroRepoCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Id)); + macroRepoCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Id)); } } diff --git a/src/Umbraco.Core/Cache/MediaCacheRefresher.cs b/src/Umbraco.Core/Cache/MediaCacheRefresher.cs index a0101ab66cc5..dae367204cd2 100644 --- a/src/Umbraco.Core/Cache/MediaCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/MediaCacheRefresher.cs @@ -56,8 +56,8 @@ public override void Refresh(JsonPayload[] payloads) // repository cache // it *was* done for each pathId but really that does not make sense // only need to do it for the current media - mediaCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Id)); - mediaCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Key)); + mediaCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Id)); + mediaCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Key)); // remove those that are in the branch if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.RefreshBranch | TreeChangeTypes.Remove)) diff --git a/src/Umbraco.Core/Cache/MemberCacheRefresher.cs b/src/Umbraco.Core/Cache/MemberCacheRefresher.cs index b416889363fa..121dbea73819 100644 --- a/src/Umbraco.Core/Cache/MemberCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/MemberCacheRefresher.cs @@ -75,8 +75,8 @@ private void ClearCache(params JsonPayload[] payloads) _idKeyMap.ClearCache(p.Id); if (memberCache) { - memberCache.Result.Clear(RepositoryCacheKeys.GetKey(p.Id)); - memberCache.Result.Clear(RepositoryCacheKeys.GetKey(p.Username)); + memberCache.Result.Clear(RepositoryCacheKeys.GetKey(p.Id)); + memberCache.Result.Clear(RepositoryCacheKeys.GetKey(p.Username)); } } diff --git a/src/Umbraco.Core/Cache/RelationTypeCacheRefresher.cs b/src/Umbraco.Core/Cache/RelationTypeCacheRefresher.cs index 6f15d095541d..a9694c92f54a 100644 --- a/src/Umbraco.Core/Cache/RelationTypeCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/RelationTypeCacheRefresher.cs @@ -32,7 +32,7 @@ public override void RefreshAll() public override void Refresh(int id) { var cache = AppCaches.IsolatedCaches.Get(); - if (cache) cache.Result.Clear(RepositoryCacheKeys.GetKey(id)); + if (cache) cache.Result.Clear(RepositoryCacheKeys.GetKey(id)); base.Refresh(id); } @@ -45,7 +45,7 @@ public override void Refresh(Guid id) public override void Remove(int id) { var cache = AppCaches.IsolatedCaches.Get(); - if (cache) cache.Result.Clear(RepositoryCacheKeys.GetKey(id)); + if (cache) cache.Result.Clear(RepositoryCacheKeys.GetKey(id)); base.Remove(id); } diff --git a/src/Umbraco.Core/Cache/UserCacheRefresher.cs b/src/Umbraco.Core/Cache/UserCacheRefresher.cs index 201ecc1f193b..706ed8ae45dd 100644 --- a/src/Umbraco.Core/Cache/UserCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/UserCacheRefresher.cs @@ -40,7 +40,7 @@ public override void Remove(int id) var userCache = AppCaches.IsolatedCaches.Get(); if (userCache) { - userCache.Result.Clear(RepositoryCacheKeys.GetKey(id)); + userCache.Result.Clear(RepositoryCacheKeys.GetKey(id)); userCache.Result.ClearByKey(CacheKeys.UserContentStartNodePathsPrefix + id); userCache.Result.ClearByKey(CacheKeys.UserMediaStartNodePathsPrefix + id); userCache.Result.ClearByKey(CacheKeys.UserAllContentStartNodesPrefix + id); diff --git a/src/Umbraco.Core/Cache/UserGroupCacheRefresher.cs b/src/Umbraco.Core/Cache/UserGroupCacheRefresher.cs index 2d278972ecd6..3a7c8d12b1b3 100644 --- a/src/Umbraco.Core/Cache/UserGroupCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/UserGroupCacheRefresher.cs @@ -55,7 +55,7 @@ public override void Remove(int id) var userGroupCache = AppCaches.IsolatedCaches.Get(); if (userGroupCache) { - userGroupCache.Result.Clear(RepositoryCacheKeys.GetKey(id)); + userGroupCache.Result.Clear(RepositoryCacheKeys.GetKey(id)); userGroupCache.Result.ClearByKey(CacheKeys.UserGroupGetByAliasCacheKeyPrefix); } diff --git a/src/Umbraco.Core/Persistence/Repositories/RepositoryCacheKeys.cs b/src/Umbraco.Core/Persistence/Repositories/RepositoryCacheKeys.cs index 5f6a20d29d6c..035ca6f4ecd3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RepositoryCacheKeys.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RepositoryCacheKeys.cs @@ -8,14 +8,30 @@ namespace Umbraco.Cms.Core.Persistence.Repositories /// public static class RepositoryCacheKeys { - private static readonly Dictionary Keys = new Dictionary(); + // used to cache keys so we don't keep allocating strings + private static readonly Dictionary s_keys = new Dictionary(); public static string GetKey() { - var type = typeof(T); - return Keys.TryGetValue(type, out var key) ? key : (Keys[type] = "uRepo_" + type.Name + "_"); + Type type = typeof(T); + return s_keys.TryGetValue(type, out var key) ? key : (s_keys[type] = "uRepo_" + type.Name + "_"); } - public static string GetKey(object id) => GetKey() + id.ToString().ToUpperInvariant(); + public static string GetKey(TId id) + { + if (EqualityComparer.Default.Equals(id, default)) + { + return string.Empty; + } + + if (typeof(TId).IsValueType) + { + return GetKey() + id; + } + else + { + return GetKey() + id.ToString().ToUpperInvariant(); + } + } } } diff --git a/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs b/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs index 106451d32a89..ef7255a0d663 100644 --- a/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs +++ b/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; @@ -24,7 +24,7 @@ namespace Umbraco.Cms.Core.Cache public class DefaultRepositoryCachePolicy : RepositoryCachePolicyBase where TEntity : class, IEntity { - private static readonly TEntity[] EmptyEntities = new TEntity[0]; // const + private static readonly TEntity[] s_emptyEntities = new TEntity[0]; // const private readonly RepositoryCachePolicyOptions _options; public DefaultRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, RepositoryCachePolicyOptions options) @@ -33,21 +33,29 @@ public DefaultRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeA _options = options ?? throw new ArgumentNullException(nameof(options)); } - protected string GetEntityCacheKey(object id) - { - if (id == null) throw new ArgumentNullException(nameof(id)); - return GetEntityTypeCacheKey() + id; - } + protected string GetEntityCacheKey(int id) => EntityTypeCacheKey + id; - protected string GetEntityTypeCacheKey() + protected string GetEntityCacheKey(TId id) { - return $"uRepo_{typeof (TEntity).Name}_"; + if (EqualityComparer.Default.Equals(id, default)) + { + return string.Empty; + } + + if (typeof(TId).IsValueType) + { + return EntityTypeCacheKey + id; + } + else + { + return EntityTypeCacheKey + id.ToString().ToUpperInvariant(); + } } + protected string EntityTypeCacheKey { get; } = $"uRepo_{typeof(TEntity).Name}_"; + protected virtual void InsertEntity(string cacheKey, TEntity entity) - { - Cache.Insert(cacheKey, () => entity, TimeSpan.FromMinutes(5), true); - } + => Cache.Insert(cacheKey, () => entity, TimeSpan.FromMinutes(5), true); protected virtual void InsertEntities(TId[] ids, TEntity[] entities) { @@ -56,7 +64,7 @@ protected virtual void InsertEntities(TId[] ids, TEntity[] entities) // getting all of them, and finding nothing. // if we can cache a zero count, cache an empty array, // for as long as the cache is not cleared (no expiration) - Cache.Insert(GetEntityTypeCacheKey(), () => EmptyEntities); + Cache.Insert(EntityTypeCacheKey, () => s_emptyEntities); } else { @@ -85,7 +93,7 @@ public override void Create(TEntity entity, Action persistNew) } // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(GetEntityTypeCacheKey()); + Cache.Clear(EntityTypeCacheKey); } catch { @@ -95,7 +103,7 @@ public override void Create(TEntity entity, Action persistNew) Cache.Clear(GetEntityCacheKey(entity.Id)); // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(GetEntityTypeCacheKey()); + Cache.Clear(EntityTypeCacheKey); throw; } @@ -117,7 +125,7 @@ public override void Update(TEntity entity, Action persistUpdated) } // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(GetEntityTypeCacheKey()); + Cache.Clear(EntityTypeCacheKey); } catch { @@ -127,7 +135,7 @@ public override void Update(TEntity entity, Action persistUpdated) Cache.Clear(GetEntityCacheKey(entity.Id)); // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(GetEntityTypeCacheKey()); + Cache.Clear(EntityTypeCacheKey); throw; } @@ -148,7 +156,7 @@ public override void Delete(TEntity entity, Action persistDeleted) var cacheKey = GetEntityCacheKey(entity.Id); Cache.Clear(cacheKey); // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(GetEntityTypeCacheKey()); + Cache.Clear(EntityTypeCacheKey); } } @@ -160,11 +168,16 @@ public override TEntity Get(TId id, Func performGet, Func> pe else { // get everything we have - var entities = Cache.GetCacheItemsByKeySearch(GetEntityTypeCacheKey()) + var entities = Cache.GetCacheItemsByKeySearch(EntityTypeCacheKey) .ToArray(); // no need for null checks, we are not caching nulls if (entities.Length > 0) @@ -222,7 +235,7 @@ public override TEntity[] GetAll(TId[] ids, Func> pe { // if none of them were in the cache // and we allow zero count - check for the special (empty) entry - var empty = Cache.GetCacheItem(GetEntityTypeCacheKey()); + var empty = Cache.GetCacheItem(EntityTypeCacheKey); if (empty != null) return empty; } } @@ -242,7 +255,7 @@ public override TEntity[] GetAll(TId[] ids, Func> pe /// public override void ClearAll() { - Cache.ClearByKey(GetEntityTypeCacheKey()); + Cache.ClearByKey(EntityTypeCacheKey); } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ConsentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ConsentRepository.cs index a64828ee5427..87a112fe08e7 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ConsentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ConsentRepository.cs @@ -85,7 +85,7 @@ protected override void PersistUpdatedItem(IConsent entity) Database.Update(dto); entity.ResetDirtyProperties(); - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Id)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Id)); } /// diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs index b953bc5b5587..17f0a8101a56 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs @@ -176,8 +176,8 @@ protected override void PersistUpdatedItem(IDictionaryItem entity) entity.ResetDirtyProperties(); //Clear the cache entries that exist by uniqueid/item key - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey)); - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key)); } protected override void PersistDeletedItem(IDictionaryItem entity) @@ -188,8 +188,8 @@ protected override void PersistDeletedItem(IDictionaryItem entity) Database.Delete("WHERE id = @Id", new { Id = entity.Key }); //Clear the cache entries that exist by uniqueid/item key - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey)); - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key)); entity.DeleteDate = DateTime.Now; } @@ -205,8 +205,8 @@ private void RecursiveDelete(Guid parentId) Database.Delete("WHERE id = @Id", new { Id = dto.UniqueId }); //Clear the cache entries that exist by uniqueid/item key - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.Key)); - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.UniqueId)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.Key)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.UniqueId)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs index a0e651f1ddfc..63a823c7b543 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs @@ -1176,7 +1176,7 @@ private IEnumerable MapDtosToContent(List dtos, if (withCache) { // if the cache contains the (proper version of the) item, use it - var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); + var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); if (cached != null && cached.VersionId == dto.DocumentVersionDto.ContentVersionDto.Id) { content[i] = (Content)cached; diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs index 3efe35e4c4e4..ec808cb04260 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs @@ -511,7 +511,7 @@ private IEnumerable MapDtosToContent(List dtos, bool withCac if (withCache) { // if the cache contains the (proper version of the) item, use it - var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); + var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); if (cached != null && cached.VersionId == dto.ContentVersionDto.Id) { content[i] = (Core.Models.Media) cached; diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs index e97add3f5e63..4c107d2a01c1 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs @@ -615,7 +615,7 @@ private IEnumerable MapDtosToContent(List dtos, bool withCac if (withCache) { // if the cache contains the (proper version of the) item, use it - var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); + var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); if (cached != null && cached.VersionId == dto.ContentVersionDto.Id) { content[i] = (Member)cached; diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs index 93700549ac81..3fae13d1172a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs @@ -86,6 +86,14 @@ private string DefaultPasswordConfigJson protected override IUser PerformGet(int id) { + // This will never resolve to a user, yet this is asked + // for all of the time (especially in cases of members). + // Don't issue a SQL call for this, we know it will not exist. + if (id == default || id < -1) + { + return null; + } + var sql = SqlContext.Sql() .Select() .From() diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index d12d7969fdf4..ce6f4a8df305 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -6,7 +6,10 @@ using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Infrastructure.DependencyInjection; using Umbraco.Cms.Web.BackOffice.ModelsBuilder; +using Microsoft.Identity.Web; using Umbraco.Extensions; +using Umbraco.Cms.Web.BackOffice.Security; +using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Hosting; namespace Umbraco.Cms.Web.UI.NetCore @@ -44,6 +47,68 @@ public void ConfigureServices(IServiceCollection services) #pragma warning disable IDE0022 // Use expression body for methods services.AddUmbraco(_env, _config) .AddBackOffice() + .AddBackOfficeExternalLogins(logins => + { + var loginProviderOptions = new BackOfficeExternalLoginProviderOptions( + "btn-google", + "fa-google", + new ExternalSignInAutoLinkOptions(true) + { + OnAutoLinking = (user, login) => + { + }, + OnExternalLogin = (user, login) => + { + user.Claims.Add(new IdentityUserClaim + { + ClaimType = "hello", + ClaimValue = "world" + }); + return true; + } + }, + denyLocalLogin: false, + autoRedirectLoginToExternalProvider: false); + + logins.AddBackOfficeLogin( + loginProviderOptions, + auth => + { + auth.AddGoogle( + auth.SchemeForBackOffice("Google"), // The scheme must be set with this method to work for the back office + options => + { + // By default this is '/signin-google' but it needs to be changed to this + options.CallbackPath = "/umbraco-google-signin"; + options.ClientId = "1072120697051-p41pro11srud3o3n90j7m00geq426jqt.apps.googleusercontent.com"; + options.ClientSecret = "cs_LJTXh2rtI01C5OIt9WFkt"; + }); + + // NOTE: Adding additional providers here is possible via the API but + // it will mean that the same BackOfficeExternalLoginProviderOptions will be registered + // for them. In some weird cases maybe people would want that? + }); + + //logins.AddBackOfficeLogin( + // new BackOfficeExternalLoginProviderOptions("btn-microsoft", "fa-windows"), + // auth => + // { + // auth.AddMicrosoftIdentityWebApp( + // options => + // { + // options.SaveTokens = true; + + // // By default this is '/signin-oidc' but it needs to be changed to this + // options.CallbackPath = "/umbraco-signin-oidc"; + // options.Instance = "https://login.microsoftonline.com/"; + // options.TenantId = "3bb0b4c5-364f-4394-ad36-0f29f95e5ddd"; + // options.ClientId = "56e98cad-ed2d-4f1b-8f85-bef11adc163f"; + // options.ClientSecret = "-1E9_fdPHi_ZkSQOb2.O5LG025sv6-NQ3h"; + // }, + // openIdConnectScheme: auth.SchemeForBackOffice("AzureAD"), // The scheme must be set with this method to work for the back office + // cookieScheme: "Fake"); + // }); + }) .AddWebsite() .AddComposers() .Build(); From afb802259a49f92e10371656426bbf17125c6e1f Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 20 Apr 2021 14:35:50 +1000 Subject: [PATCH 26/26] bah, far out this keeps getting recommitted. sorry --- src/Umbraco.Web.UI.NetCore/Startup.cs | 69 +-------------------------- 1 file changed, 1 insertion(+), 68 deletions(-) diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index ce6f4a8df305..38db76323074 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -4,12 +4,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Infrastructure.DependencyInjection; -using Umbraco.Cms.Web.BackOffice.ModelsBuilder; -using Microsoft.Identity.Web; using Umbraco.Extensions; -using Umbraco.Cms.Web.BackOffice.Security; -using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Hosting; namespace Umbraco.Cms.Web.UI.NetCore @@ -46,69 +41,7 @@ public void ConfigureServices(IServiceCollection services) { #pragma warning disable IDE0022 // Use expression body for methods services.AddUmbraco(_env, _config) - .AddBackOffice() - .AddBackOfficeExternalLogins(logins => - { - var loginProviderOptions = new BackOfficeExternalLoginProviderOptions( - "btn-google", - "fa-google", - new ExternalSignInAutoLinkOptions(true) - { - OnAutoLinking = (user, login) => - { - }, - OnExternalLogin = (user, login) => - { - user.Claims.Add(new IdentityUserClaim - { - ClaimType = "hello", - ClaimValue = "world" - }); - return true; - } - }, - denyLocalLogin: false, - autoRedirectLoginToExternalProvider: false); - - logins.AddBackOfficeLogin( - loginProviderOptions, - auth => - { - auth.AddGoogle( - auth.SchemeForBackOffice("Google"), // The scheme must be set with this method to work for the back office - options => - { - // By default this is '/signin-google' but it needs to be changed to this - options.CallbackPath = "/umbraco-google-signin"; - options.ClientId = "1072120697051-p41pro11srud3o3n90j7m00geq426jqt.apps.googleusercontent.com"; - options.ClientSecret = "cs_LJTXh2rtI01C5OIt9WFkt"; - }); - - // NOTE: Adding additional providers here is possible via the API but - // it will mean that the same BackOfficeExternalLoginProviderOptions will be registered - // for them. In some weird cases maybe people would want that? - }); - - //logins.AddBackOfficeLogin( - // new BackOfficeExternalLoginProviderOptions("btn-microsoft", "fa-windows"), - // auth => - // { - // auth.AddMicrosoftIdentityWebApp( - // options => - // { - // options.SaveTokens = true; - - // // By default this is '/signin-oidc' but it needs to be changed to this - // options.CallbackPath = "/umbraco-signin-oidc"; - // options.Instance = "https://login.microsoftonline.com/"; - // options.TenantId = "3bb0b4c5-364f-4394-ad36-0f29f95e5ddd"; - // options.ClientId = "56e98cad-ed2d-4f1b-8f85-bef11adc163f"; - // options.ClientSecret = "-1E9_fdPHi_ZkSQOb2.O5LG025sv6-NQ3h"; - // }, - // openIdConnectScheme: auth.SchemeForBackOffice("AzureAD"), // The scheme must be set with this method to work for the back office - // cookieScheme: "Fake"); - // }); - }) + .AddBackOffice() .AddWebsite() .AddComposers() .Build();