From e32e35d3af7956f9e785b4baec106d59991a3f7a Mon Sep 17 00:00:00 2001 From: Masu-Baumgartner <68913099+Masu-Baumgartner@users.noreply.github.com> Date: Tue, 1 Oct 2024 14:27:09 +0200 Subject: [PATCH] Implemented api/check endpoint. Added api error middleware --- .../Exceptions/MissingPermissionException.cs | 11 ++ .../Authentication/PermClaimsPrinciple.cs | 7 +- .../Http/Controllers/Auth/AuthController.cs | 17 +++ .../Http/Middleware/ApiErrorMiddleware.cs | 72 +++++++++ .../Middleware/AuthorisationMiddleware.cs | 76 --------- .../Middleware/AuthorizationMiddleware.cs | 144 ++++++++++++++++++ Moonlight.ApiServer/Program.cs | 2 + .../Http/Responses/Auth/CheckResponse.cs | 8 + 8 files changed, 258 insertions(+), 79 deletions(-) create mode 100644 Moonlight.ApiServer/Exceptions/MissingPermissionException.cs create mode 100644 Moonlight.ApiServer/Http/Middleware/ApiErrorMiddleware.cs delete mode 100644 Moonlight.ApiServer/Http/Middleware/AuthorisationMiddleware.cs create mode 100644 Moonlight.ApiServer/Http/Middleware/AuthorizationMiddleware.cs create mode 100644 Moonlight.Shared/Http/Responses/Auth/CheckResponse.cs diff --git a/Moonlight.ApiServer/Exceptions/MissingPermissionException.cs b/Moonlight.ApiServer/Exceptions/MissingPermissionException.cs new file mode 100644 index 00000000..35cc1af4 --- /dev/null +++ b/Moonlight.ApiServer/Exceptions/MissingPermissionException.cs @@ -0,0 +1,11 @@ +namespace Moonlight.ApiServer.Exceptions; + +public class MissingPermissionException : Exception +{ + public string Permission { get; set; } + + public MissingPermissionException(string permission) + { + Permission = permission; + } +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Helpers/Authentication/PermClaimsPrinciple.cs b/Moonlight.ApiServer/Helpers/Authentication/PermClaimsPrinciple.cs index 9c08253e..833e0e4f 100644 --- a/Moonlight.ApiServer/Helpers/Authentication/PermClaimsPrinciple.cs +++ b/Moonlight.ApiServer/Helpers/Authentication/PermClaimsPrinciple.cs @@ -6,12 +6,13 @@ namespace Moonlight.ApiServer.Helpers.Authentication; public class PermClaimsPrinciple : ClaimsPrincipal { public string[] Permissions { get; private set; } - public User? CurrentModel { get; private set; } + public User? CurrentModelNullable { get; private set; } + public User CurrentModel => CurrentModelNullable!; - public PermClaimsPrinciple(string[] permissions, User? currentModel) + public PermClaimsPrinciple(string[] permissions, User? currentModelNullable) { Permissions = permissions; - CurrentModel = currentModel; + CurrentModelNullable = currentModelNullable; } public bool HasPermission(string requiredPermission) diff --git a/Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs b/Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs index 685942f7..55bf5154 100644 --- a/Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs +++ b/Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs @@ -1,4 +1,6 @@ using Microsoft.AspNetCore.Mvc; +using Moonlight.ApiServer.Attributes; +using Moonlight.ApiServer.Helpers.Authentication; using Moonlight.ApiServer.Services; using Moonlight.Shared.Http.Requests.Auth; using Moonlight.Shared.Http.Responses.Auth; @@ -41,4 +43,19 @@ public class AuthController : Controller Token = await AuthService.GenerateToken(user) }; } + + [HttpGet("check")] + [RequirePermission("meta.authenticated")] + public async Task Check() + { + var perm = HttpContext.User as PermClaimsPrinciple; + var user = perm!.CurrentModel; + + return new CheckResponse() + { + Email = user.Email, + Username = user.Username, + Permissions = perm.Permissions + }; + } } \ No newline at end of file diff --git a/Moonlight.ApiServer/Http/Middleware/ApiErrorMiddleware.cs b/Moonlight.ApiServer/Http/Middleware/ApiErrorMiddleware.cs new file mode 100644 index 00000000..7a0cecd1 --- /dev/null +++ b/Moonlight.ApiServer/Http/Middleware/ApiErrorMiddleware.cs @@ -0,0 +1,72 @@ +using System.Diagnostics; +using System.Net.Sockets; +using MoonCore.Exceptions; + +namespace Moonlight.ApiServer.Http.Middleware; + +public class ApiErrorMiddleware +{ + private readonly RequestDelegate Next; + + public ApiErrorMiddleware(RequestDelegate next) + { + Next = next; + } + + public async Task Invoke(HttpContext context) + { + try + { + await Next(context); + } + catch (HttpApiException httpApiException) + { + await Results.Problem( + title: httpApiException.Title, + detail: httpApiException.Detail, + statusCode: httpApiException.Status, + type: "moonlight/general-api-error" + ).ExecuteAsync(context); + } + catch (HttpRequestException e) + { + var logger = context.RequestServices.GetRequiredService>(); + + if (e.InnerException is SocketException) + { + logger.LogCritical("An unhandled socket exception occured. [{method}] {path}: {e}", context.Request.Method, context.Request.Path, e); + + await Results.Problem( + title: "An socket exception occured on the api server", + detail: "Check the api server logs for more details", + statusCode: 502, + type: "moonlight/remote-api-connection-error" + ).ExecuteAsync(context); + + return; + } + + logger.LogCritical("An unhandled exception occured. [{method}] {path}: {e}", context.Request.Method, context.Request.Path, e.Demystify()); + + await Results.Problem( + title: "An http request exception occured on the api server", + detail: "Check the api server logs for more details", + statusCode: 500, + type: "moonlight/remote-api-request-error" + ).ExecuteAsync(context); + } + catch (Exception e) + { + var logger = context.RequestServices.GetRequiredService>(); + + logger.LogCritical("An unhandled exception occured. [{method}] {path}: {e}", context.Request.Method, context.Request.Path, e); + + await Results.Problem( + title: "An unhanded exception occured on the api server", + detail: "Check the api server logs for more details", + statusCode: 500, + type: "moonlight/critical-api-error" + ).ExecuteAsync(context); + } + } +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Http/Middleware/AuthorisationMiddleware.cs b/Moonlight.ApiServer/Http/Middleware/AuthorisationMiddleware.cs deleted file mode 100644 index 7218bee5..00000000 --- a/Moonlight.ApiServer/Http/Middleware/AuthorisationMiddleware.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Microsoft.AspNetCore.Mvc.Controllers; -using Moonlight.ApiServer.Attributes; - -namespace Moonlight.ApiServer.Http.Middleware; - -public class AuthorisationMiddleware -{ - private readonly RequestDelegate Next; - private readonly ILogger Logger; - - public AuthorisationMiddleware(RequestDelegate next, ILogger logger) - { - Next = next; - Logger = logger; - } - - public async Task InvokeAsync(HttpContext context) - { - await Next(context); - } - - private async Task Authorize(HttpContext context) - { - - } - - 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/AuthorizationMiddleware.cs b/Moonlight.ApiServer/Http/Middleware/AuthorizationMiddleware.cs new file mode 100644 index 00000000..8ca662f7 --- /dev/null +++ b/Moonlight.ApiServer/Http/Middleware/AuthorizationMiddleware.cs @@ -0,0 +1,144 @@ +using Microsoft.AspNetCore.Mvc.Controllers; +using Moonlight.ApiServer.Attributes; +using Moonlight.ApiServer.Exceptions; +using Moonlight.ApiServer.Helpers.Authentication; + +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.CurrentModelNullable == 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 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/Program.cs b/Moonlight.ApiServer/Program.cs index 70be7ed4..3ca36c66 100644 --- a/Moonlight.ApiServer/Program.cs +++ b/Moonlight.ApiServer/Program.cs @@ -117,7 +117,9 @@ app.UseStaticFiles(); app.UseRouting(); +app.UseMiddleware(); app.UseMiddleware(); +app.UseMiddleware(); app.MapControllers(); diff --git a/Moonlight.Shared/Http/Responses/Auth/CheckResponse.cs b/Moonlight.Shared/Http/Responses/Auth/CheckResponse.cs new file mode 100644 index 00000000..b9b48f24 --- /dev/null +++ b/Moonlight.Shared/Http/Responses/Auth/CheckResponse.cs @@ -0,0 +1,8 @@ +namespace Moonlight.Shared.Http.Responses.Auth; + +public class CheckResponse +{ + public string Username { get; set; } + public string Email { get; set; } + public string[] Permissions { get; set; } +} \ No newline at end of file