From 7265d5c3beda3ff7fe718b6dc5abd4fb345c071d Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 28 Jun 2023 08:40:28 +0200 Subject: [PATCH] Use OpenIddict with real db instead of inmemory (#14465) * Add OpenIddict tables to database (#14449) * Added migrations to install EF Core OpenIddict tables * Handle Install of ef core data (Needs to be outside of transaction * Cleanup and renaming, as these things will be reused for more than openiddict in the future * Cleanup * Extract db context setup * Minor cleanup --------- Co-authored-by: Nikolaj * Use OpenIddict from DB instead of InMemoryDb * Do not try to clean up, while not it run mode * Fixed tests * Clean up --------- Co-authored-by: Nikolaj Co-authored-by: Elitsa --- .../BackOfficeAuthBuilderExtensions.cs | 60 ++++--------------- .../ManagementApiComposer.cs | 32 +++++----- ...ceAuthorizationInitializationMiddleware.cs | 24 ++++---- .../Security/BackOfficeApplicationManager.cs | 11 +++- .../Umbraco.Cms.Api.Management.csproj | 1 - .../HostedServices/OpenIddictCleanup.cs | 11 +++- src/Umbraco.Web.UI.New.Client | 2 +- .../NewBackoffice/OpenAPIContractTest.cs | 13 ++++ 8 files changed, 74 insertions(+), 80 deletions(-) diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthBuilderExtensions.cs index 8ed635ad38..236e5e45c0 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthBuilderExtensions.cs @@ -1,7 +1,6 @@ using Microsoft.AspNetCore.Authorization; -using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using OpenIddict.Validation.AspNetCore; using Umbraco.Cms.Core; using Umbraco.Cms.Core.DependencyInjection; @@ -10,6 +9,7 @@ using Umbraco.Cms.Api.Management.Security; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Cms.Infrastructure.HostedServices; using Umbraco.Cms.Infrastructure.Security; +using Umbraco.Cms.Web.Common.ApplicationBuilder; namespace Umbraco.Cms.Api.Management.DependencyInjection; @@ -18,43 +18,18 @@ public static class BackOfficeAuthBuilderExtensions public static IUmbracoBuilder AddBackOfficeAuthentication(this IUmbracoBuilder builder) { builder - .AddDbContext() .AddOpenIddict() .AddBackOfficeLogin(); return builder; } - private static IUmbracoBuilder AddDbContext(this IUmbracoBuilder builder) - { - builder.Services.AddDbContext(options => - { - // Configure the DB context - // TODO: use actual Umbraco DbContext once EF is implemented - and remove dependency on Microsoft.EntityFrameworkCore.InMemory - options.UseInMemoryDatabase(nameof(DbContext)); - - // Register the entity sets needed by OpenIddict. - options.UseOpenIddict(); - }); - - return builder; - } - private static IUmbracoBuilder AddOpenIddict(this IUmbracoBuilder builder) { builder.Services.AddAuthentication(); builder.Services.AddAuthorization(CreatePolicies); builder.Services.AddOpenIddict() - - // Register the OpenIddict core components. - .AddCore(options => - { - options - .UseEntityFrameworkCore() - .UseDbContext(); - }) - // Register the OpenIddict server components. .AddServer(options => { @@ -118,9 +93,9 @@ public static class BackOfficeAuthBuilderExtensions builder.Services.AddTransient(); builder.Services.AddSingleton(); + builder.Services.Configure(options => options.AddFilter(new BackofficePipelineFilter("Backoffice"))); builder.Services.AddHostedService(); - builder.Services.AddHostedService(); return builder; } @@ -138,28 +113,6 @@ public static class BackOfficeAuthBuilderExtensions return builder; } - // TODO: remove this once EF is implemented - public class DatabaseManager : IHostedService - { - private readonly IServiceProvider _serviceProvider; - - public DatabaseManager(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; - - public async Task StartAsync(CancellationToken cancellationToken) - { - using IServiceScope scope = _serviceProvider.CreateScope(); - - DbContext context = scope.ServiceProvider.GetRequiredService(); - await context.Database.EnsureCreatedAsync(cancellationToken); - - // TODO: add BackOfficeAuthorizationInitializationMiddleware before UseAuthorization (to make it run for unauthorized API requests) and remove this - IBackOfficeApplicationManager backOfficeApplicationManager = scope.ServiceProvider.GetRequiredService(); - await backOfficeApplicationManager.EnsureBackOfficeApplicationAsync(new Uri("https://" + - "localhost:44339/"), cancellationToken); - } - - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; - } // TODO: move this to an appropriate location and implement the policy scheme that should be used for the new management APIs private static void CreatePolicies(AuthorizationOptions options) @@ -186,3 +139,10 @@ public static class BackOfficeAuthBuilderExtensions AddPolicy(AuthorizationPolicies.SectionAccessContentOrMedia, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Content, Constants.Applications.Media); } } + +internal class BackofficePipelineFilter : UmbracoPipelineFilter +{ + public BackofficePipelineFilter(string name) + : base(name) + => PrePipeline = builder => builder.UseMiddleware(); +} diff --git a/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs b/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs index 7abcd170e8..699c663db6 100644 --- a/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs +++ b/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs @@ -10,6 +10,7 @@ using Umbraco.Cms.Api.Management.Serialization; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models.Configuration; using Umbraco.Cms.Web.Common.ApplicationBuilder; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management; @@ -21,21 +22,20 @@ public class ManagementApiComposer : IComposer IServiceCollection services = builder.Services; - builder - .AddJson() - .AddNewInstaller() - .AddUpgrader() - .AddSearchManagement() - .AddTrees() - .AddAuditLogs() - .AddDocuments() - .AddDocumentTypes() - .AddMedia() - .AddMediaTypes() - .AddLanguages() - .AddDictionary() - .AddHealthChecks() - .AddModelsBuilder() + ModelsBuilderBuilderExtensions.AddModelsBuilder(builder + .AddJson() + .AddNewInstaller() + .AddUpgrader() + .AddSearchManagement() + .AddTrees() + .AddAuditLogs() + .AddDocuments() + .AddDocumentTypes() + .AddMedia() + .AddMediaTypes() + .AddLanguages() + .AddDictionary() + .AddHealthChecks()) .AddRedirectUrl() .AddTags() .AddTrackedReferences() @@ -80,6 +80,8 @@ public class ManagementApiComposer : IComposer builder.AddUmbracoOptions(); // FIXME: remove this when NewBackOfficeSettings is moved to core services.AddSingleton, NewBackOfficeSettingsValidator>(); + + BackOfficeAuthBuilderOpenIddictExtensions.AddUmbracoEFCoreDbContext(builder); } } diff --git a/src/Umbraco.Cms.Api.Management/Middleware/BackOfficeAuthorizationInitializationMiddleware.cs b/src/Umbraco.Cms.Api.Management/Middleware/BackOfficeAuthorizationInitializationMiddleware.cs index 755261e3b6..bd5395d0d6 100644 --- a/src/Umbraco.Cms.Api.Management/Middleware/BackOfficeAuthorizationInitializationMiddleware.cs +++ b/src/Umbraco.Cms.Api.Management/Middleware/BackOfficeAuthorizationInitializationMiddleware.cs @@ -1,7 +1,9 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Security; namespace Umbraco.Cms.Api.Management.Middleware; @@ -13,11 +15,16 @@ public class BackOfficeAuthorizationInitializationMiddleware : IMiddleware private readonly UmbracoRequestPaths _umbracoRequestPaths; private readonly IServiceProvider _serviceProvider; + private readonly IRuntimeState _runtimeState; - public BackOfficeAuthorizationInitializationMiddleware(UmbracoRequestPaths umbracoRequestPaths, IServiceProvider serviceProvider) + public BackOfficeAuthorizationInitializationMiddleware( + UmbracoRequestPaths umbracoRequestPaths, + IServiceProvider serviceProvider, + IRuntimeState runtimeState) { _umbracoRequestPaths = umbracoRequestPaths; _serviceProvider = serviceProvider; + _runtimeState = runtimeState; } public async Task InvokeAsync(HttpContext context, RequestDelegate next) @@ -33,6 +40,11 @@ public class BackOfficeAuthorizationInitializationMiddleware : IMiddleware return; } + if (_runtimeState.Level < RuntimeLevel.Run) + { + return; + } + if (_umbracoRequestPaths.IsBackOfficeRequest(context.Request.Path) == false) { return; @@ -50,13 +62,3 @@ public class BackOfficeAuthorizationInitializationMiddleware : IMiddleware _firstBackOfficeRequestLocker.Release(); } } - -// TODO: remove this (used for testing BackOfficeAuthorizationInitializationMiddleware until it can be added to the existing UseBackOffice extension) -// public static class UmbracoApplicationBuilderExtensions -// { -// public static IUmbracoApplicationBuilderContext UseNewBackOffice(this IUmbracoApplicationBuilderContext builder) -// { -// builder.AppBuilder.UseMiddleware(); -// return builder; -// } -// } diff --git a/src/Umbraco.Cms.Api.Management/Security/BackOfficeApplicationManager.cs b/src/Umbraco.Cms.Api.Management/Security/BackOfficeApplicationManager.cs index a46610e795..091eecadfe 100644 --- a/src/Umbraco.Cms.Api.Management/Security/BackOfficeApplicationManager.cs +++ b/src/Umbraco.Cms.Api.Management/Security/BackOfficeApplicationManager.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Options; using OpenIddict.Abstractions; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models.Configuration; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Security; namespace Umbraco.Cms.Api.Management.Security; @@ -12,22 +13,30 @@ public class BackOfficeApplicationManager : IBackOfficeApplicationManager { private readonly IOpenIddictApplicationManager _applicationManager; private readonly IWebHostEnvironment _webHostEnvironment; + private readonly IRuntimeState _runtimeState; private readonly Uri? _backOfficeHost; private readonly string? _authorizeCallbackPathName; public BackOfficeApplicationManager( IOpenIddictApplicationManager applicationManager, IWebHostEnvironment webHostEnvironment, - IOptions securitySettings) + IOptions securitySettings, + IRuntimeState runtimeState) { _applicationManager = applicationManager; _webHostEnvironment = webHostEnvironment; + _runtimeState = runtimeState; _backOfficeHost = securitySettings.Value.BackOfficeHost; _authorizeCallbackPathName = securitySettings.Value.AuthorizeCallbackPathName; } public async Task EnsureBackOfficeApplicationAsync(Uri backOfficeUrl, CancellationToken cancellationToken = default) { + if (_runtimeState.Level < RuntimeLevel.Run) + { + return; + } + if (backOfficeUrl.IsAbsoluteUri is false) { throw new ArgumentException($"Expected an absolute URL, got: {backOfficeUrl}", nameof(backOfficeUrl)); diff --git a/src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj b/src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj index 604636b4cb..c6e8ca3a93 100644 --- a/src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj +++ b/src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj @@ -10,7 +10,6 @@ - diff --git a/src/Umbraco.Infrastructure/HostedServices/OpenIddictCleanup.cs b/src/Umbraco.Infrastructure/HostedServices/OpenIddictCleanup.cs index 785c60c828..c65f687e16 100644 --- a/src/Umbraco.Infrastructure/HostedServices/OpenIddictCleanup.cs +++ b/src/Umbraco.Infrastructure/HostedServices/OpenIddictCleanup.cs @@ -2,6 +2,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using OpenIddict.Abstractions; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Infrastructure.HostedServices; @@ -14,17 +16,24 @@ public class OpenIddictCleanup : RecurringHostedServiceBase private readonly ILogger _logger; private readonly IServiceProvider _provider; + private readonly IRuntimeState _runtimeState; public OpenIddictCleanup( - ILogger logger, IServiceProvider provider) + ILogger logger, IServiceProvider provider, IRuntimeState runtimeState) : base(logger, TimeSpan.FromHours(1), TimeSpan.FromMinutes(5)) { _logger = logger; _provider = provider; + _runtimeState = runtimeState; } public override async Task PerformExecuteAsync(object? state) { + if (_runtimeState.Level < RuntimeLevel.Run) + { + return; + } + // hosted services are registered as singletons, but this particular one consumes scoped services... so // we have to fetch the service dependencies manually using a new scope per invocation. IServiceScope scope = _provider.CreateScope(); diff --git a/src/Umbraco.Web.UI.New.Client b/src/Umbraco.Web.UI.New.Client index b422069597..90f9c709cb 160000 --- a/src/Umbraco.Web.UI.New.Client +++ b/src/Umbraco.Web.UI.New.Client @@ -1 +1 @@ -Subproject commit b422069597cf7cefc071b875c3176cdddf68a26c +Subproject commit 90f9c709cb55a7ac97073327b0fc19b55a9da153 diff --git a/tests/Umbraco.Tests.Integration/NewBackoffice/OpenAPIContractTest.cs b/tests/Umbraco.Tests.Integration/NewBackoffice/OpenAPIContractTest.cs index 314768d42e..1b5a2992b3 100644 --- a/tests/Umbraco.Tests.Integration/NewBackoffice/OpenAPIContractTest.cs +++ b/tests/Umbraco.Tests.Integration/NewBackoffice/OpenAPIContractTest.cs @@ -7,7 +7,9 @@ using Umbraco.Cms.Api.Management; using Umbraco.Cms.Api.Management.Controllers.Install; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Persistence.EFCore.Composition; using Umbraco.Cms.Tests.Integration.TestServerTest; +using Umbraco.Cms.Web.Common.ApplicationBuilder; namespace Umbraco.Cms.Tests.Integration.NewBackoffice; @@ -27,6 +29,17 @@ internal sealed class OpenAPIContractTest : UmbracoTestServerTestBase }); new ManagementApiComposer().Compose(builder); + new UmbracoEFCoreComposer().Compose(builder); + + // Currently we cannot do this in tests, as EF Core is not initialized + builder.Services.PostConfigure(options => + { + var backofficePipelineFilter = options.PipelineFilters.FirstOrDefault(x => x.Name.Equals("Backoffice")); + if (backofficePipelineFilter != null) + { + options.PipelineFilters.Remove(backofficePipelineFilter); + } + }); } [Test]