From 3dd5d2958a92ba1bdd7c07a08ed911b0d088bb3a Mon Sep 17 00:00:00 2001 From: ChiaraBm Date: Mon, 24 Feb 2025 20:03:37 +0100 Subject: [PATCH] Implemented plugin loading via di on the api server. Fixed plugin loading in the client --- .../Middleware/ApiAuthenticationMiddleware.cs | 67 -------- .../Middleware/AuthorizationMiddleware.cs | 147 ------------------ .../Middleware/PermissionLoaderMiddleware.cs | 33 ---- .../Implementations/Startup/ApiDocsStartup.cs | 46 ------ .../Startup/CoreAssetStartup.cs | 24 --- .../Startup/CoreDatabaseStartup.cs | 15 -- .../Implementations/Startup/CoreStartup.cs | 66 ++++++++ .../Interfaces/Startup/IAppStartup.cs | 7 - .../Interfaces/Startup/IDatabaseStartup.cs | 8 - .../Interfaces/Startup/IEndpointStartup.cs | 6 - .../Interfaces/Startup/IPluginStartup.cs | 11 ++ .../Moonlight.ApiServer.csproj | 4 +- Moonlight.ApiServer/Startup.cs | 74 +++++---- .../Implementations/CoreStartup.cs | 17 ++ Moonlight.Client/Moonlight.Client.csproj | 6 +- Moonlight.Client/Startup.cs | 38 ++--- Moonlight.Client/UI/App.razor | 1 - Moonlight.Client/UI/Partials/AppSidebar.razor | 2 +- 18 files changed, 157 insertions(+), 415 deletions(-) delete mode 100644 Moonlight.ApiServer/Http/Middleware/ApiAuthenticationMiddleware.cs delete mode 100644 Moonlight.ApiServer/Http/Middleware/AuthorizationMiddleware.cs delete mode 100644 Moonlight.ApiServer/Http/Middleware/PermissionLoaderMiddleware.cs delete mode 100644 Moonlight.ApiServer/Implementations/Startup/ApiDocsStartup.cs delete mode 100644 Moonlight.ApiServer/Implementations/Startup/CoreAssetStartup.cs delete mode 100644 Moonlight.ApiServer/Implementations/Startup/CoreDatabaseStartup.cs create mode 100644 Moonlight.ApiServer/Implementations/Startup/CoreStartup.cs delete mode 100644 Moonlight.ApiServer/Interfaces/Startup/IAppStartup.cs delete mode 100644 Moonlight.ApiServer/Interfaces/Startup/IDatabaseStartup.cs delete mode 100644 Moonlight.ApiServer/Interfaces/Startup/IEndpointStartup.cs create mode 100644 Moonlight.ApiServer/Interfaces/Startup/IPluginStartup.cs create mode 100644 Moonlight.Client/Implementations/CoreStartup.cs diff --git a/Moonlight.ApiServer/Http/Middleware/ApiAuthenticationMiddleware.cs b/Moonlight.ApiServer/Http/Middleware/ApiAuthenticationMiddleware.cs deleted file mode 100644 index 507002c8..00000000 --- a/Moonlight.ApiServer/Http/Middleware/ApiAuthenticationMiddleware.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Text.Json; -using MoonCore.Authentication; -using MoonCore.Extended.Abstractions; -using Moonlight.ApiServer.Database.Entities; - -namespace Moonlight.ApiServer.Http.Middleware; - -public class ApiAuthenticationMiddleware -{ - private readonly RequestDelegate Next; - private readonly ILogger Logger; - - public ApiAuthenticationMiddleware(RequestDelegate next, ILogger logger) - { - Next = next; - Logger = logger; - } - - public async Task InvokeAsync(HttpContext context) - { - await Authenticate(context); - await Next(context); - } - - public Task Authenticate(HttpContext context) - { - var request = context.Request; - - if(!request.Headers.ContainsKey("Authorization")) - return Task.CompletedTask; - - if(request.Headers["Authorization"].Count == 0) - return Task.CompletedTask; - - var authHeader = request.Headers["Authorization"].First(); - - if(string.IsNullOrEmpty(authHeader)) - return Task.CompletedTask; - - var parts = authHeader.Split(" "); - - if(parts.Length != 2) - return Task.CompletedTask; - - var bearerValue = parts[1]; - - if(!bearerValue.StartsWith("api_")) - return Task.CompletedTask; - - if(bearerValue.Length != "api_".Length + 32) - return Task.CompletedTask; - - var apiKeyRepo = context.RequestServices.GetRequiredService>(); - var apiKey = apiKeyRepo.Get().FirstOrDefault(x => x.Secret == bearerValue); - - if(apiKey == null) - return Task.CompletedTask; - - var permissions = JsonSerializer.Deserialize(apiKey.PermissionsJson) ?? []; - context.User = new PermClaimsPrinciple() - { - Permissions = permissions - }; - - return Task.CompletedTask; - } -} \ No newline at end of file diff --git a/Moonlight.ApiServer/Http/Middleware/AuthorizationMiddleware.cs b/Moonlight.ApiServer/Http/Middleware/AuthorizationMiddleware.cs deleted file mode 100644 index 323bf7bf..00000000 --- a/Moonlight.ApiServer/Http/Middleware/AuthorizationMiddleware.cs +++ /dev/null @@ -1,147 +0,0 @@ -using Microsoft.AspNetCore.Mvc.Controllers; -using MoonCore.Attributes; -using MoonCore.Authentication; -using MoonCore.Extensions; -using Moonlight.ApiServer.Exceptions; - -namespace Moonlight.ApiServer.Http.Middleware; - -public class AuthorizationMiddleware -{ - private readonly RequestDelegate Next; - private readonly ILogger Logger; - - public AuthorizationMiddleware(RequestDelegate next, ILogger logger) - { - Next = next; - Logger = logger; - } - - public async Task InvokeAsync(HttpContext context) - { - if (await Authorize(context)) - { - try - { - await Next(context); - } - catch (MissingPermissionException e) - { - if (e.Permission == "meta.authenticated") - { - await Results.Problem( - title: "This endpoint requires a user authenticated token", - statusCode: 401 - ).ExecuteAsync(context); - } - else - { - await Results.Problem( - title: "You dont have the required permission", - detail: e.Permission, - statusCode: 403 - ).ExecuteAsync(context); - } - } - } - } - - private async Task Authorize(HttpContext context) - { - var requiredPermissions = ResolveRequiredPermissions(context); - - if (requiredPermissions.Length == 0) - return true; - - // Check if no context => permissions have been loaded - if (context.User is not PermClaimsPrinciple permClaimsPrinciple) - { - await Results.Problem( - title: "An unauthenticated request is not allowed to use this endpoint", - statusCode: 401 - ).ExecuteAsync(context); - - return false; - } - - // Check if one of the required permissions is to be logged in - if (requiredPermissions.Any(x => x == "meta.authenticated") && permClaimsPrinciple.IdentityModel == null) - { - await Results.Problem( - title: "This endpoint requires a user authenticated token", - statusCode: 401 - ).ExecuteAsync(context); - - return false; - } - - foreach (var permission in requiredPermissions) - { - if(permission == "meta.authenticated") // We already verified that - continue; - - if (!permClaimsPrinciple.HasPermission(permission)) - { - await Results.Problem( - title: "You dont have the required permission", - detail: permission, - statusCode: 403 - ).ExecuteAsync(context); - - return false; - } - } - - return true; - } - - private string[] ResolveRequiredPermissions(HttpContext context) - { - // Basic handling - var endpoint = context.GetEndpoint(); - - if (endpoint == null) - return []; - - var metadata = endpoint - .Metadata - .GetMetadata(); - - if (metadata == null) - return []; - - // Retrieve attribute infos - var controllerAttrInfo = metadata - .ControllerTypeInfo - .CustomAttributes - .FirstOrDefault(x => x.AttributeType == typeof(RequirePermissionAttribute)); - - var methodAttrInfo = metadata - .MethodInfo - .CustomAttributes - .FirstOrDefault(x => x.AttributeType == typeof(RequirePermissionAttribute)); - - // Retrieve permissions from attribute infos - var controllerPermission = controllerAttrInfo != null - ? controllerAttrInfo.ConstructorArguments.First().Value as string - : null; - - var methodPermission = methodAttrInfo != null - ? methodAttrInfo.ConstructorArguments.First().Value as string - : null; - - // If both have a permission flag, return both - if (controllerPermission != null && methodPermission != null) - return [controllerPermission, methodPermission]; - - // If either of them have a permission set, return it - if (controllerPermission != null) - return [controllerPermission]; - - if (methodPermission != null) - return [methodPermission]; - - // If both have no permission set, allow everyone to access it - return []; - } -} \ No newline at end of file diff --git a/Moonlight.ApiServer/Http/Middleware/PermissionLoaderMiddleware.cs b/Moonlight.ApiServer/Http/Middleware/PermissionLoaderMiddleware.cs deleted file mode 100644 index ecacc11b..00000000 --- a/Moonlight.ApiServer/Http/Middleware/PermissionLoaderMiddleware.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Text.Json; -using MoonCore.Authentication; -using Moonlight.ApiServer.Database.Entities; - -namespace Moonlight.ApiServer.Http.Middleware; - -public class PermissionLoaderMiddleware -{ - private readonly RequestDelegate Next; - - public PermissionLoaderMiddleware(RequestDelegate next) - { - Next = next; - } - - public async Task Invoke(HttpContext context) - { - await Load(context); - await Next(context); - } - - private Task Load(HttpContext context) - { - if(context.User is not PermClaimsPrinciple permClaimsPrinciple) - return Task.CompletedTask; - - if(permClaimsPrinciple.IdentityModel is not User user) - return Task.CompletedTask; - - permClaimsPrinciple.Permissions = JsonSerializer.Deserialize(user.PermissionsJson) ?? []; - return Task.CompletedTask; - } -} \ No newline at end of file diff --git a/Moonlight.ApiServer/Implementations/Startup/ApiDocsStartup.cs b/Moonlight.ApiServer/Implementations/Startup/ApiDocsStartup.cs deleted file mode 100644 index 390cde0b..00000000 --- a/Moonlight.ApiServer/Implementations/Startup/ApiDocsStartup.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Microsoft.OpenApi.Models; -using MoonCore.Services; -using Moonlight.ApiServer.Configuration; -using Moonlight.ApiServer.Interfaces.Startup; - -namespace Moonlight.ApiServer.Implementations.Startup; - -public class ApiDocsStartup : IAppStartup, IEndpointStartup -{ - private readonly ILogger Logger; - private readonly AppConfiguration AppConfiguration; - - public ApiDocsStartup(ILogger logger, AppConfiguration appConfiguration) - { - Logger = logger; - AppConfiguration = appConfiguration; - } - - public Task BuildApp(IHostApplicationBuilder builder) - { - if(!AppConfiguration.Development.EnableApiDocs) - return Task.CompletedTask; - - builder.Services.AddEndpointsApiExplorer(); - - // Configure swagger api specification generator and set the document title for the api docs to use - builder.Services.AddSwaggerGen(options => options.SwaggerDoc("main", new OpenApiInfo() - { - Title = "Moonlight API" - })); - - return Task.CompletedTask; - } - - public Task ConfigureApp(IApplicationBuilder app) => Task.CompletedTask; - - public Task ConfigureEndpoints(IEndpointRouteBuilder routeBuilder) - { - if(!AppConfiguration.Development.EnableApiDocs) - return Task.CompletedTask; - - routeBuilder.MapSwagger("/api/swagger/{documentName}"); - - return Task.CompletedTask; - } -} \ No newline at end of file diff --git a/Moonlight.ApiServer/Implementations/Startup/CoreAssetStartup.cs b/Moonlight.ApiServer/Implementations/Startup/CoreAssetStartup.cs deleted file mode 100644 index 9ca53575..00000000 --- a/Moonlight.ApiServer/Implementations/Startup/CoreAssetStartup.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Moonlight.ApiServer.Interfaces.Startup; -using Moonlight.ApiServer.Services; - -namespace Moonlight.ApiServer.Implementations.Startup; - -public class CoreAssetStartup : IAppStartup -{ - private readonly BundleService BundleService; - - public CoreAssetStartup(BundleService bundleService) - { - BundleService = bundleService; - } - - public Task BuildApp(IHostApplicationBuilder builder) - { - BundleService.BundleCss("css/core.min.css"); - - return Task.CompletedTask; - } - - public Task ConfigureApp(IApplicationBuilder app) - => Task.CompletedTask; -} \ No newline at end of file diff --git a/Moonlight.ApiServer/Implementations/Startup/CoreDatabaseStartup.cs b/Moonlight.ApiServer/Implementations/Startup/CoreDatabaseStartup.cs deleted file mode 100644 index 7023d041..00000000 --- a/Moonlight.ApiServer/Implementations/Startup/CoreDatabaseStartup.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Moonlight.ApiServer.Database; -using Moonlight.ApiServer.Helpers; -using Moonlight.ApiServer.Interfaces.Startup; - -namespace Moonlight.ApiServer.Implementations.Startup; - -public class CoreDatabaseStartup : IDatabaseStartup -{ - public Task ConfigureDatabase(DatabaseContextCollection collection) - { - collection.Add(); - - return Task.CompletedTask; - } -} \ No newline at end of file diff --git a/Moonlight.ApiServer/Implementations/Startup/CoreStartup.cs b/Moonlight.ApiServer/Implementations/Startup/CoreStartup.cs new file mode 100644 index 00000000..b38011f4 --- /dev/null +++ b/Moonlight.ApiServer/Implementations/Startup/CoreStartup.cs @@ -0,0 +1,66 @@ +using Microsoft.OpenApi.Models; +using Moonlight.ApiServer.Configuration; +using Moonlight.ApiServer.Database; +using Moonlight.ApiServer.Helpers; +using Moonlight.ApiServer.Interfaces.Startup; +using Moonlight.ApiServer.Services; + +namespace Moonlight.ApiServer.Implementations.Startup; + +public class CoreStartup : IPluginStartup +{ + private readonly AppConfiguration Configuration; + private readonly BundleService BundleService; + + public CoreStartup(AppConfiguration configuration, BundleService bundleService) + { + Configuration = configuration; + BundleService = bundleService; + } + + public Task BuildApplication(IHostApplicationBuilder builder) + { + #region Api Docs + + if (Configuration.Development.EnableApiDocs) + { + builder.Services.AddEndpointsApiExplorer(); + + // Configure swagger api specification generator and set the document title for the api docs to use + builder.Services.AddSwaggerGen(options => options.SwaggerDoc("main", new OpenApiInfo() + { + Title = "Moonlight API" + })); + } + + #endregion + + #region Assets + + BundleService.BundleCss("css/core.min.css"); + + #endregion + + return Task.CompletedTask; + } + + public Task ConfigureApplication(IApplicationBuilder app) + { + return Task.CompletedTask; + } + + public Task ConfigureDatabase(DatabaseContextCollection collection) + { + collection.Add(); + + return Task.CompletedTask; + } + + public Task ConfigureEndpoints(IEndpointRouteBuilder routeBuilder) + { + if(Configuration.Development.EnableApiDocs) + routeBuilder.MapSwagger("/api/swagger/{documentName}"); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Interfaces/Startup/IAppStartup.cs b/Moonlight.ApiServer/Interfaces/Startup/IAppStartup.cs deleted file mode 100644 index 34b9ddfc..00000000 --- a/Moonlight.ApiServer/Interfaces/Startup/IAppStartup.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Moonlight.ApiServer.Interfaces.Startup; - -public interface IAppStartup -{ - public Task BuildApp(IHostApplicationBuilder builder); - public Task ConfigureApp(IApplicationBuilder app); -} \ No newline at end of file diff --git a/Moonlight.ApiServer/Interfaces/Startup/IDatabaseStartup.cs b/Moonlight.ApiServer/Interfaces/Startup/IDatabaseStartup.cs deleted file mode 100644 index 676392bf..00000000 --- a/Moonlight.ApiServer/Interfaces/Startup/IDatabaseStartup.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Moonlight.ApiServer.Helpers; - -namespace Moonlight.ApiServer.Interfaces.Startup; - -public interface IDatabaseStartup -{ - public Task ConfigureDatabase(DatabaseContextCollection collection); -} \ No newline at end of file diff --git a/Moonlight.ApiServer/Interfaces/Startup/IEndpointStartup.cs b/Moonlight.ApiServer/Interfaces/Startup/IEndpointStartup.cs deleted file mode 100644 index d963b9ab..00000000 --- a/Moonlight.ApiServer/Interfaces/Startup/IEndpointStartup.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Moonlight.ApiServer.Interfaces.Startup; - -public interface IEndpointStartup -{ - public Task ConfigureEndpoints(IEndpointRouteBuilder routeBuilder); -} \ No newline at end of file diff --git a/Moonlight.ApiServer/Interfaces/Startup/IPluginStartup.cs b/Moonlight.ApiServer/Interfaces/Startup/IPluginStartup.cs new file mode 100644 index 00000000..d97e4741 --- /dev/null +++ b/Moonlight.ApiServer/Interfaces/Startup/IPluginStartup.cs @@ -0,0 +1,11 @@ +using Moonlight.ApiServer.Helpers; + +namespace Moonlight.ApiServer.Interfaces.Startup; + +public interface IPluginStartup +{ + public Task BuildApplication(IHostApplicationBuilder builder); + public Task ConfigureApplication(IApplicationBuilder app); + public Task ConfigureDatabase(DatabaseContextCollection collection); + public Task ConfigureEndpoints(IEndpointRouteBuilder routeBuilder); +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Moonlight.ApiServer.csproj b/Moonlight.ApiServer/Moonlight.ApiServer.csproj index 8cef2652..2362b1f3 100644 --- a/Moonlight.ApiServer/Moonlight.ApiServer.csproj +++ b/Moonlight.ApiServer/Moonlight.ApiServer.csproj @@ -24,8 +24,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/Moonlight.ApiServer/Startup.cs b/Moonlight.ApiServer/Startup.cs index 72b82a1e..2788fbba 100644 --- a/Moonlight.ApiServer/Startup.cs +++ b/Moonlight.ApiServer/Startup.cs @@ -53,9 +53,7 @@ public class Startup // Asset bundling private BundleService BundleService; - private IAppStartup[] PluginAppStartups; - private IDatabaseStartup[] PluginDatabaseStartups; - private IEndpointStartup[] PluginEndpointStartups; + private IPluginStartup[] PluginStartups; public async Task Run(string[] args, Assembly[]? additionalAssemblies = null) { @@ -303,35 +301,53 @@ public class Startup private Task InitializePlugins() { - var initialisationServiceCollection = new ServiceCollection(); + // Define minimal service collection + var startupSc = new ServiceCollection(); // Configure base services for initialisation - initialisationServiceCollection.AddSingleton(Configuration); + startupSc.AddSingleton(Configuration); BundleService = new BundleService(); - initialisationServiceCollection.AddSingleton(BundleService); + startupSc.AddSingleton(BundleService); - initialisationServiceCollection.AddLogging(builder => { builder.AddProviders(LoggerProviders); }); - - // Configure plugin loading by using the interface service - initialisationServiceCollection.AddInterfaces(configuration => + startupSc.AddLogging(builder => { - // We use moonlight itself as a plugin assembly - configuration.AddAssembly(typeof(Startup).Assembly); - - configuration.AddAssemblies(PluginLoaderService.PluginAssemblies); - configuration.AddAssemblies(AdditionalAssemblies); - - configuration.AddInterface(); - configuration.AddInterface(); - configuration.AddInterface(); + builder.ClearProviders(); + builder.AddProviders(LoggerProviders); }); + + // + var startupSp = startupSc.BuildServiceProvider(); + + // Initialize plugin startups + var startups = new List(); + var startupType = typeof(IPluginStartup); - var initialisationServiceProvider = initialisationServiceCollection.BuildServiceProvider(); + var assembliesToScan = new List(); + + assembliesToScan.Add(typeof(Startup).Assembly); + assembliesToScan.AddRange(PluginLoaderService.PluginAssemblies); + assembliesToScan.AddRange(AdditionalAssemblies); + + foreach (var pluginAssembly in assembliesToScan) + { + var startupTypes = pluginAssembly + .ExportedTypes + .Where(x => !x.IsAbstract && !x.IsInterface && x.IsAssignableTo(startupType)) + .ToArray(); - PluginAppStartups = initialisationServiceProvider.GetRequiredService(); - PluginDatabaseStartups = initialisationServiceProvider.GetRequiredService(); - PluginEndpointStartups = initialisationServiceProvider.GetRequiredService(); + foreach (var type in startupTypes) + { + var startup = ActivatorUtilities.CreateInstance(startupSp, type) as IPluginStartup; + + if(startup == null) + continue; + + startups.Add(startup); + } + } + + PluginStartups = startups.ToArray(); return Task.CompletedTask; } @@ -364,11 +380,11 @@ public class Startup private async Task HookPluginBuild() { - foreach (var pluginAppStartup in PluginAppStartups) + foreach (var pluginAppStartup in PluginStartups) { try { - await pluginAppStartup.BuildApp(WebApplicationBuilder); + await pluginAppStartup.BuildApplication(WebApplicationBuilder); } catch (Exception e) { @@ -383,11 +399,11 @@ public class Startup private async Task HookPluginConfigure() { - foreach (var pluginAppStartup in PluginAppStartups) + foreach (var pluginAppStartup in PluginStartups) { try { - await pluginAppStartup.ConfigureApp(WebApplication); + await pluginAppStartup.ConfigureApplication(WebApplication); } catch (Exception e) { @@ -402,7 +418,7 @@ public class Startup private async Task HookPluginEndpoints() { - foreach (var pluginEndpointStartup in PluginEndpointStartups) + foreach (var pluginEndpointStartup in PluginStartups) { try { @@ -555,7 +571,7 @@ public class Startup var databaseCollection = new DatabaseContextCollection(); - foreach (var databaseStartup in PluginDatabaseStartups) + foreach (var databaseStartup in PluginStartups) await databaseStartup.ConfigureDatabase(databaseCollection); foreach (var database in databaseCollection) diff --git a/Moonlight.Client/Implementations/CoreStartup.cs b/Moonlight.Client/Implementations/CoreStartup.cs new file mode 100644 index 00000000..c7f59b59 --- /dev/null +++ b/Moonlight.Client/Implementations/CoreStartup.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using Moonlight.Client.Interfaces; + +namespace Moonlight.Client.Implementations; + +public class CoreStartup : IPluginStartup +{ + public Task BuildApplication(WebAssemblyHostBuilder builder) + { + builder.Services.AddSingleton(); + + return Task.CompletedTask; + } + + public Task ConfigureApplication(WebAssemblyHost app) + => Task.CompletedTask; +} \ No newline at end of file diff --git a/Moonlight.Client/Moonlight.Client.csproj b/Moonlight.Client/Moonlight.Client.csproj index e75ba64e..dea6d9b3 100644 --- a/Moonlight.Client/Moonlight.Client.csproj +++ b/Moonlight.Client/Moonlight.Client.csproj @@ -24,10 +24,10 @@ - - + + - +