Implemented api/check endpoint. Added api error middleware
This commit is contained in:
11
Moonlight.ApiServer/Exceptions/MissingPermissionException.cs
Normal file
11
Moonlight.ApiServer/Exceptions/MissingPermissionException.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace Moonlight.ApiServer.Exceptions;
|
||||||
|
|
||||||
|
public class MissingPermissionException : Exception
|
||||||
|
{
|
||||||
|
public string Permission { get; set; }
|
||||||
|
|
||||||
|
public MissingPermissionException(string permission)
|
||||||
|
{
|
||||||
|
Permission = permission;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,12 +6,13 @@ namespace Moonlight.ApiServer.Helpers.Authentication;
|
|||||||
public class PermClaimsPrinciple : ClaimsPrincipal
|
public class PermClaimsPrinciple : ClaimsPrincipal
|
||||||
{
|
{
|
||||||
public string[] Permissions { get; private set; }
|
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;
|
Permissions = permissions;
|
||||||
CurrentModel = currentModel;
|
CurrentModelNullable = currentModelNullable;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool HasPermission(string requiredPermission)
|
public bool HasPermission(string requiredPermission)
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Moonlight.ApiServer.Attributes;
|
||||||
|
using Moonlight.ApiServer.Helpers.Authentication;
|
||||||
using Moonlight.ApiServer.Services;
|
using Moonlight.ApiServer.Services;
|
||||||
using Moonlight.Shared.Http.Requests.Auth;
|
using Moonlight.Shared.Http.Requests.Auth;
|
||||||
using Moonlight.Shared.Http.Responses.Auth;
|
using Moonlight.Shared.Http.Responses.Auth;
|
||||||
@@ -41,4 +43,19 @@ public class AuthController : Controller
|
|||||||
Token = await AuthService.GenerateToken(user)
|
Token = await AuthService.GenerateToken(user)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("check")]
|
||||||
|
[RequirePermission("meta.authenticated")]
|
||||||
|
public async Task<CheckResponse> Check()
|
||||||
|
{
|
||||||
|
var perm = HttpContext.User as PermClaimsPrinciple;
|
||||||
|
var user = perm!.CurrentModel;
|
||||||
|
|
||||||
|
return new CheckResponse()
|
||||||
|
{
|
||||||
|
Email = user.Email,
|
||||||
|
Username = user.Username,
|
||||||
|
Permissions = perm.Permissions
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
72
Moonlight.ApiServer/Http/Middleware/ApiErrorMiddleware.cs
Normal file
72
Moonlight.ApiServer/Http/Middleware/ApiErrorMiddleware.cs
Normal file
@@ -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<ILogger<ApiErrorMiddleware>>();
|
||||||
|
|
||||||
|
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<ILogger<ApiErrorMiddleware>>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<AuthorisationMiddleware> Logger;
|
|
||||||
|
|
||||||
public AuthorisationMiddleware(RequestDelegate next, ILogger<AuthorisationMiddleware> 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<ControllerActionDescriptor>();
|
|
||||||
|
|
||||||
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 [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
144
Moonlight.ApiServer/Http/Middleware/AuthorizationMiddleware.cs
Normal file
144
Moonlight.ApiServer/Http/Middleware/AuthorizationMiddleware.cs
Normal file
@@ -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<AuthorizationMiddleware> Logger;
|
||||||
|
|
||||||
|
public AuthorizationMiddleware(RequestDelegate next, ILogger<AuthorizationMiddleware> 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<bool> 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<ControllerActionDescriptor>();
|
||||||
|
|
||||||
|
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 [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -117,7 +117,9 @@ app.UseStaticFiles();
|
|||||||
|
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
|
|
||||||
|
app.UseMiddleware<ApiErrorMiddleware>();
|
||||||
app.UseMiddleware<AuthenticationMiddleware>();
|
app.UseMiddleware<AuthenticationMiddleware>();
|
||||||
|
app.UseMiddleware<AuthorizationMiddleware>();
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
|
|||||||
8
Moonlight.Shared/Http/Responses/Auth/CheckResponse.cs
Normal file
8
Moonlight.Shared/Http/Responses/Auth/CheckResponse.cs
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user