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 <nikolajlauridsen@protonmail.ch>

* 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 <nikolajlauridsen@protonmail.ch>
Co-authored-by: Elitsa <elm@umbraco.dk>
This commit is contained in:
Bjarke Berg
2023-06-28 08:40:28 +02:00
committed by GitHub
parent dfc7054720
commit 7265d5c3be
8 changed files with 74 additions and 80 deletions

View File

@@ -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<DbContext>(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<DbContext>();
})
// Register the OpenIddict server components.
.AddServer(options =>
{
@@ -118,9 +93,9 @@ public static class BackOfficeAuthBuilderExtensions
builder.Services.AddTransient<IBackOfficeApplicationManager, BackOfficeApplicationManager>();
builder.Services.AddSingleton<BackOfficeAuthorizationInitializationMiddleware>();
builder.Services.Configure<UmbracoPipelineOptions>(options => options.AddFilter(new BackofficePipelineFilter("Backoffice")));
builder.Services.AddHostedService<OpenIddictCleanup>();
builder.Services.AddHostedService<DatabaseManager>();
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<DbContext>();
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<IBackOfficeApplicationManager>();
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<BackOfficeAuthorizationInitializationMiddleware>();
}

View File

@@ -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<NewBackOfficeSettings>();
// FIXME: remove this when NewBackOfficeSettings is moved to core
services.AddSingleton<IValidateOptions<NewBackOfficeSettings>, NewBackOfficeSettingsValidator>();
BackOfficeAuthBuilderOpenIddictExtensions.AddUmbracoEFCoreDbContext(builder);
}
}

View File

@@ -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<BackOfficeAuthorizationInitializationMiddleware>();
// return builder;
// }
// }

View File

@@ -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<NewBackOfficeSettings> securitySettings)
IOptions<NewBackOfficeSettings> 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));

View File

@@ -10,7 +10,6 @@
<ItemGroup>
<PackageReference Include="JsonPatch.Net" Version="2.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.2" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="4.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>

View File

@@ -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<OpenIddictCleanup> _logger;
private readonly IServiceProvider _provider;
private readonly IRuntimeState _runtimeState;
public OpenIddictCleanup(
ILogger<OpenIddictCleanup> logger, IServiceProvider provider)
ILogger<OpenIddictCleanup> 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();

View File

@@ -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<UmbracoPipelineOptions>(options =>
{
var backofficePipelineFilter = options.PipelineFilters.FirstOrDefault(x => x.Name.Equals("Backoffice"));
if (backofficePipelineFilter != null)
{
options.PipelineFilters.Remove(backofficePipelineFilter);
}
});
}
[Test]