[backend] Replace mastodon-specific middleware with modes triggered on MastodonApiControllerAttribute
This commit is contained in:
parent
cb749c94e6
commit
e31a0719f4
16 changed files with 155 additions and 165 deletions
|
@ -0,0 +1,5 @@
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||||
|
|
||||||
|
public class MastodonApiControllerAttribute : ApiControllerAttribute;
|
|
@ -1,3 +1,4 @@
|
||||||
|
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Renderers;
|
using Iceshrimp.Backend.Controllers.Mastodon.Renderers;
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
||||||
|
@ -10,21 +11,20 @@ using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Mastodon;
|
namespace Iceshrimp.Backend.Controllers.Mastodon;
|
||||||
|
|
||||||
[ApiController]
|
[MastodonApiController]
|
||||||
[Tags("Mastodon")]
|
|
||||||
[Route("/api/v1/accounts")]
|
[Route("/api/v1/accounts")]
|
||||||
[AuthenticateOauth]
|
[Authenticate]
|
||||||
[EnableRateLimiting("sliding")]
|
[EnableRateLimiting("sliding")]
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
public class MastodonAccountController(DatabaseContext db, UserRenderer userRenderer) : Controller {
|
public class MastodonAccountController(DatabaseContext db, UserRenderer userRenderer) : Controller {
|
||||||
[AuthorizeOauth("read:accounts")]
|
[Authorize("read:accounts")]
|
||||||
[HttpGet("verify_credentials")]
|
[HttpGet("verify_credentials")]
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Account))]
|
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Account))]
|
||||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
|
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))]
|
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))]
|
||||||
public async Task<IActionResult> VerifyUserCredentials() {
|
public async Task<IActionResult> VerifyUserCredentials() {
|
||||||
var user = HttpContext.GetOauthUser() ?? throw new GracefulException("Failed to get user from HttpContext");
|
var user = HttpContext.GetUser() ?? throw new GracefulException("Failed to get user from HttpContext");
|
||||||
var res = await userRenderer.RenderAsync(user);
|
var res = await userRenderer.RenderAsync(user);
|
||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
||||||
using Iceshrimp.Backend.Core.Database;
|
using Iceshrimp.Backend.Core.Database;
|
||||||
using Iceshrimp.Backend.Core.Database.Tables;
|
using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
|
@ -10,13 +11,12 @@ using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Mastodon;
|
namespace Iceshrimp.Backend.Controllers.Mastodon;
|
||||||
|
|
||||||
[ApiController]
|
[MastodonApiController]
|
||||||
[Tags("Mastodon")]
|
|
||||||
[EnableRateLimiting("sliding")]
|
[EnableRateLimiting("sliding")]
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
public class MastodonAuthController(DatabaseContext db) : Controller {
|
public class MastodonAuthController(DatabaseContext db) : Controller {
|
||||||
[HttpGet("/api/v1/apps/verify_credentials")]
|
[HttpGet("/api/v1/apps/verify_credentials")]
|
||||||
[AuthenticateOauth]
|
[Authenticate]
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AuthSchemas.VerifyAppCredentialsResponse))]
|
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AuthSchemas.VerifyAppCredentialsResponse))]
|
||||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
|
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Renderers;
|
using Iceshrimp.Backend.Controllers.Mastodon.Renderers;
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
||||||
|
@ -10,15 +11,14 @@ using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Mastodon;
|
namespace Iceshrimp.Backend.Controllers.Mastodon;
|
||||||
|
|
||||||
[ApiController]
|
[MastodonApiController]
|
||||||
[Tags("Mastodon")]
|
|
||||||
[Route("/api/v1/statuses")]
|
[Route("/api/v1/statuses")]
|
||||||
[AuthenticateOauth]
|
[Authenticate]
|
||||||
[EnableRateLimiting("sliding")]
|
[EnableRateLimiting("sliding")]
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
public class MastodonStatusController(DatabaseContext db, NoteRenderer noteRenderer) : Controller {
|
public class MastodonStatusController(DatabaseContext db, NoteRenderer noteRenderer) : Controller {
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
[AuthenticateOauth("read:statuses")]
|
[Authenticate("read:statuses")]
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Account))]
|
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Account))]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using Iceshrimp.Backend.Controllers.Attributes;
|
using Iceshrimp.Backend.Controllers.Attributes;
|
||||||
|
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Renderers;
|
using Iceshrimp.Backend.Controllers.Mastodon.Renderers;
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
||||||
|
@ -11,22 +12,21 @@ using Microsoft.AspNetCore.RateLimiting;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Mastodon;
|
namespace Iceshrimp.Backend.Controllers.Mastodon;
|
||||||
|
|
||||||
[ApiController]
|
[MastodonApiController]
|
||||||
[Tags("Mastodon")]
|
|
||||||
[Route("/api/v1/timelines")]
|
[Route("/api/v1/timelines")]
|
||||||
[AuthenticateOauth]
|
[Authenticate]
|
||||||
[LinkPagination(20, 40)]
|
[LinkPagination(20, 40)]
|
||||||
[EnableRateLimiting("sliding")]
|
[EnableRateLimiting("sliding")]
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
|
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))]
|
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))]
|
||||||
public class MastodonTimelineController(DatabaseContext db, NoteRenderer noteRenderer) : Controller {
|
public class MastodonTimelineController(DatabaseContext db, NoteRenderer noteRenderer) : Controller {
|
||||||
[AuthorizeOauth("read:statuses")]
|
[Authorize("read:statuses")]
|
||||||
[HttpGet("home")]
|
[HttpGet("home")]
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<Status>))]
|
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<Status>))]
|
||||||
public async Task<IActionResult> GetHomeTimeline(PaginationQuery query) {
|
public async Task<IActionResult> GetHomeTimeline(PaginationQuery query) {
|
||||||
var user = HttpContext.GetOauthUser() ?? throw new GracefulException("Failed to get user from HttpContext");
|
var user = HttpContext.GetUser() ?? throw new GracefulException("Failed to get user from HttpContext");
|
||||||
var res = await db.Notes
|
var res = await db.Notes
|
||||||
.IncludeCommonProperties()
|
.IncludeCommonProperties()
|
||||||
.FilterByFollowingAndOwn(user)
|
.FilterByFollowingAndOwn(user)
|
||||||
|
@ -41,12 +41,12 @@ public class MastodonTimelineController(DatabaseContext db, NoteRenderer noteRen
|
||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
[AuthorizeOauth("read:statuses")]
|
[Authorize("read:statuses")]
|
||||||
[HttpGet("public")]
|
[HttpGet("public")]
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<Status>))]
|
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<Status>))]
|
||||||
public async Task<IActionResult> GetPublicTimeline(PaginationQuery query) {
|
public async Task<IActionResult> GetPublicTimeline(PaginationQuery query) {
|
||||||
var user = HttpContext.GetOauthUser() ?? throw new GracefulException("Failed to get user from HttpContext");
|
var user = HttpContext.GetUser() ?? throw new GracefulException("Failed to get user from HttpContext");
|
||||||
|
|
||||||
var res = await db.Notes
|
var res = await db.Notes
|
||||||
.IncludeCommonProperties()
|
.IncludeCommonProperties()
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
||||||
|
using JI = System.Text.Json.Serialization.JsonIgnoreAttribute;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
||||||
|
|
||||||
public class MastodonErrorResponse {
|
public class MastodonErrorResponse {
|
||||||
[J("error")] public required string Error { get; set; }
|
[J("error")] public required string Error { get; set; }
|
||||||
[J("error_description")] public required string? Description { get; set; }
|
|
||||||
|
[J("error_description")]
|
||||||
|
[JI(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public required string? Description { get; set; }
|
||||||
}
|
}
|
|
@ -32,7 +32,6 @@ public static class ServiceExtensions {
|
||||||
services.AddScoped<WebFingerService>();
|
services.AddScoped<WebFingerService>();
|
||||||
services.AddScoped<AuthorizedFetchMiddleware>();
|
services.AddScoped<AuthorizedFetchMiddleware>();
|
||||||
services.AddScoped<AuthenticationMiddleware>();
|
services.AddScoped<AuthenticationMiddleware>();
|
||||||
services.AddScoped<OauthAuthenticationMiddleware>();
|
|
||||||
|
|
||||||
//TODO: make this prettier
|
//TODO: make this prettier
|
||||||
services.AddScoped<Controllers.Mastodon.Renderers.UserRenderer>();
|
services.AddScoped<Controllers.Mastodon.Renderers.UserRenderer>();
|
||||||
|
@ -46,7 +45,6 @@ public static class ServiceExtensions {
|
||||||
services.AddSingleton<ErrorHandlerMiddleware>();
|
services.AddSingleton<ErrorHandlerMiddleware>();
|
||||||
services.AddSingleton<RequestBufferingMiddleware>();
|
services.AddSingleton<RequestBufferingMiddleware>();
|
||||||
services.AddSingleton<AuthorizationMiddleware>();
|
services.AddSingleton<AuthorizationMiddleware>();
|
||||||
services.AddSingleton<OauthAuthorizationMiddleware>();
|
|
||||||
services.AddSingleton<RequestVerificationMiddleware>();
|
services.AddSingleton<RequestVerificationMiddleware>();
|
||||||
|
|
||||||
// Hosted services = long running background tasks
|
// Hosted services = long running background tasks
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||||
using Iceshrimp.Backend.Core.Middleware;
|
using Iceshrimp.Backend.Core.Middleware;
|
||||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
|
@ -11,6 +12,23 @@ public static class SwaggerGenOptionsExtensions {
|
||||||
public static void AddOperationFilters(this SwaggerGenOptions options) {
|
public static void AddOperationFilters(this SwaggerGenOptions options) {
|
||||||
options.OperationFilter<AuthorizeCheckOperationFilter>();
|
options.OperationFilter<AuthorizeCheckOperationFilter>();
|
||||||
options.OperationFilter<HybridRequestOperationFilter>();
|
options.OperationFilter<HybridRequestOperationFilter>();
|
||||||
|
options.OperationFilter<MastodonApiControllerOperationFilter>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Local",
|
||||||
|
Justification = "SwaggerGenOptions.OperationFilter<T> instantiates this class at runtime")]
|
||||||
|
private class MastodonApiControllerOperationFilter : IOperationFilter {
|
||||||
|
public void Apply(OpenApiOperation operation, OperationFilterContext context) {
|
||||||
|
if (context.MethodInfo.DeclaringType is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var isMastodonController = context.MethodInfo.DeclaringType.GetCustomAttributes(true)
|
||||||
|
.OfType<MastodonApiControllerAttribute>().Any();
|
||||||
|
|
||||||
|
if (!isMastodonController) return;
|
||||||
|
|
||||||
|
operation.Tags = [new OpenApiTag { Name = "Mastodon" }];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Local",
|
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Local",
|
||||||
|
@ -26,17 +44,15 @@ public static class SwaggerGenOptionsExtensions {
|
||||||
context.MethodInfo.GetCustomAttributes(true)
|
context.MethodInfo.GetCustomAttributes(true)
|
||||||
.OfType<AuthenticateAttribute>().Any();
|
.OfType<AuthenticateAttribute>().Any();
|
||||||
|
|
||||||
var hasOauthAuthenticate = context.MethodInfo.DeclaringType.GetCustomAttributes(true)
|
var isMastodonController = context.MethodInfo.DeclaringType.GetCustomAttributes(true)
|
||||||
.OfType<AuthenticateOauthAttribute>().Any() ||
|
.OfType<MastodonApiControllerAttribute>().Any();
|
||||||
context.MethodInfo.GetCustomAttributes(true)
|
|
||||||
.OfType<AuthenticateOauthAttribute>().Any();
|
|
||||||
|
|
||||||
if (!hasAuthenticate && !hasOauthAuthenticate) return;
|
if (!hasAuthenticate) return;
|
||||||
|
|
||||||
var schema = new OpenApiSecurityScheme {
|
var schema = new OpenApiSecurityScheme {
|
||||||
Reference = new OpenApiReference {
|
Reference = new OpenApiReference {
|
||||||
Type = ReferenceType.SecurityScheme,
|
Type = ReferenceType.SecurityScheme,
|
||||||
Id = hasAuthenticate ? "user" : "mastodon"
|
Id = isMastodonController ? "mastodon" : "user"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -14,8 +14,6 @@ public static class WebApplicationExtensions {
|
||||||
.UseMiddleware<RequestBufferingMiddleware>()
|
.UseMiddleware<RequestBufferingMiddleware>()
|
||||||
.UseMiddleware<AuthenticationMiddleware>()
|
.UseMiddleware<AuthenticationMiddleware>()
|
||||||
.UseMiddleware<AuthorizationMiddleware>()
|
.UseMiddleware<AuthorizationMiddleware>()
|
||||||
.UseMiddleware<OauthAuthenticationMiddleware>()
|
|
||||||
.UseMiddleware<OauthAuthorizationMiddleware>()
|
|
||||||
.UseMiddleware<AuthorizedFetchMiddleware>();
|
.UseMiddleware<AuthorizedFetchMiddleware>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
|
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||||
using Iceshrimp.Backend.Core.Database;
|
using Iceshrimp.Backend.Core.Database;
|
||||||
using Iceshrimp.Backend.Core.Database.Tables;
|
using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Iceshrimp.Backend.Core.Helpers;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Middleware;
|
namespace Iceshrimp.Backend.Core.Middleware;
|
||||||
|
|
||||||
public class AuthenticationMiddleware(DatabaseContext db) : IMiddleware {
|
public class AuthenticationMiddleware(DatabaseContext db) : IMiddleware {
|
||||||
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) {
|
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) {
|
||||||
var endpoint = ctx.Features.Get<IEndpointFeature>()?.Endpoint;
|
var endpoint = ctx.GetEndpoint();
|
||||||
var attribute = endpoint?.Metadata.GetMetadata<AuthenticateAttribute>();
|
var attribute = endpoint?.Metadata.GetMetadata<AuthenticateAttribute>();
|
||||||
|
|
||||||
if (attribute != null) {
|
if (attribute != null) {
|
||||||
|
@ -19,27 +20,54 @@ public class AuthenticationMiddleware(DatabaseContext db) : IMiddleware {
|
||||||
}
|
}
|
||||||
|
|
||||||
var token = header[7..];
|
var token = header[7..];
|
||||||
var session = await db.Sessions
|
|
||||||
.Include(p => p.User)
|
|
||||||
.ThenInclude(p => p.UserProfile)
|
|
||||||
.FirstOrDefaultAsync(p => p.Token == token && p.Active);
|
|
||||||
|
|
||||||
if (session == null) {
|
var isMastodon = endpoint?.Metadata.GetMetadata<MastodonApiControllerAttribute>() != null;
|
||||||
await next(ctx);
|
if (isMastodon) {
|
||||||
return;
|
var oauthToken = await db.OauthTokens
|
||||||
|
.Include(p => p.User)
|
||||||
|
.ThenInclude(p => p.UserProfile)
|
||||||
|
.Include(p => p.App)
|
||||||
|
.FirstOrDefaultAsync(p => p.Token == header && p.Active);
|
||||||
|
|
||||||
|
if (oauthToken == null) {
|
||||||
|
await next(ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attribute.Scopes.Length > 0 &&
|
||||||
|
attribute.Scopes.Except(MastodonOauthHelpers.ExpandScopes(oauthToken.Scopes)).Any()) {
|
||||||
|
await next(ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.SetOauthToken(oauthToken);
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
var session = await db.Sessions
|
||||||
|
.Include(p => p.User)
|
||||||
|
.ThenInclude(p => p.UserProfile)
|
||||||
|
.FirstOrDefaultAsync(p => p.Token == token && p.Active);
|
||||||
|
|
||||||
ctx.SetSession(session);
|
if (session == null) {
|
||||||
|
await next(ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.SetSession(session);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await next(ctx);
|
await next(ctx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AuthenticateAttribute : Attribute;
|
public class AuthenticateAttribute(params string[] scopes) : Attribute {
|
||||||
|
public readonly string[] Scopes = scopes;
|
||||||
|
}
|
||||||
|
|
||||||
public static partial class HttpContextExtensions {
|
public static class HttpContextExtensions {
|
||||||
private const string Key = "session";
|
private const string Key = "session";
|
||||||
|
private const string MastodonKey = "masto-session";
|
||||||
|
|
||||||
internal static void SetSession(this HttpContext ctx, Session session) {
|
internal static void SetSession(this HttpContext ctx, Session session) {
|
||||||
ctx.Items.Add(Key, session);
|
ctx.Items.Add(Key, session);
|
||||||
|
@ -50,8 +78,21 @@ public static partial class HttpContextExtensions {
|
||||||
return session as Session;
|
return session as Session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static void SetOauthToken(this HttpContext ctx, OauthToken session) {
|
||||||
|
ctx.Items.Add(MastodonKey, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static OauthToken? GetOauthToken(this HttpContext ctx) {
|
||||||
|
ctx.Items.TryGetValue(MastodonKey, out var session);
|
||||||
|
return session as OauthToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Is it faster to check for the MastodonApiControllerAttribute here?
|
||||||
public static User? GetUser(this HttpContext ctx) {
|
public static User? GetUser(this HttpContext ctx) {
|
||||||
ctx.Items.TryGetValue(Key, out var session);
|
if (ctx.Items.TryGetValue(Key, out var session))
|
||||||
return (session as Session)?.User;
|
return (session as Session)?.User;
|
||||||
|
return ctx.Items.TryGetValue(MastodonKey, out var token)
|
||||||
|
? (token as OauthToken)?.User
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,19 +1,33 @@
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||||
|
using Iceshrimp.Backend.Core.Helpers;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Middleware;
|
namespace Iceshrimp.Backend.Core.Middleware;
|
||||||
|
|
||||||
public class AuthorizationMiddleware : IMiddleware {
|
public class AuthorizationMiddleware : IMiddleware {
|
||||||
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) {
|
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) {
|
||||||
var endpoint = ctx.Features.Get<IEndpointFeature>()?.Endpoint;
|
var endpoint = ctx.GetEndpoint();
|
||||||
var attribute = endpoint?.Metadata.GetMetadata<AuthorizeAttribute>();
|
var attribute = endpoint?.Metadata.GetMetadata<AuthorizeAttribute>();
|
||||||
|
|
||||||
if (attribute != null)
|
if (attribute != null) {
|
||||||
if (ctx.GetSession() is not { Active: true })
|
var isMastodon = endpoint?.Metadata.GetMetadata<MastodonApiControllerAttribute>() != null;
|
||||||
|
|
||||||
|
if (isMastodon) {
|
||||||
|
var token = ctx.GetOauthToken();
|
||||||
|
if (token is not { Active: true })
|
||||||
|
throw GracefulException.Unauthorized("This method requires an authenticated user");
|
||||||
|
if (attribute.Scopes.Length > 0 &&
|
||||||
|
attribute.Scopes.Except(MastodonOauthHelpers.ExpandScopes(token.Scopes)).Any())
|
||||||
|
throw GracefulException.Forbidden("This action is outside the authorized scopes");
|
||||||
|
}
|
||||||
|
else if (ctx.GetSession() is not { Active: true }) {
|
||||||
throw GracefulException.Forbidden("This method requires an authenticated user");
|
throw GracefulException.Forbidden("This method requires an authenticated user");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await next(ctx);
|
await next(ctx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AuthorizeAttribute : Attribute;
|
public class AuthorizeAttribute(params string[] scopes) : Attribute {
|
||||||
//TODO: oauth scopes?
|
public readonly string[] Scopes = scopes;
|
||||||
|
}
|
|
@ -5,7 +5,6 @@ using Iceshrimp.Backend.Core.Database;
|
||||||
using Iceshrimp.Backend.Core.Federation.ActivityPub;
|
using Iceshrimp.Backend.Core.Federation.ActivityPub;
|
||||||
using Iceshrimp.Backend.Core.Federation.Cryptography;
|
using Iceshrimp.Backend.Core.Federation.Cryptography;
|
||||||
using Iceshrimp.Backend.Core.Services;
|
using Iceshrimp.Backend.Core.Services;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
@ -19,8 +18,7 @@ public class AuthorizedFetchMiddleware(
|
||||||
UserService userSvc,
|
UserService userSvc,
|
||||||
ILogger<AuthorizedFetchMiddleware> logger) : IMiddleware {
|
ILogger<AuthorizedFetchMiddleware> logger) : IMiddleware {
|
||||||
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) {
|
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) {
|
||||||
var endpoint = ctx.Features.Get<IEndpointFeature>()?.Endpoint;
|
var attribute = ctx.GetEndpoint()?.Metadata.GetMetadata<AuthorizedFetchAttribute>();
|
||||||
var attribute = endpoint?.Metadata.GetMetadata<AuthorizedFetchAttribute>();
|
|
||||||
|
|
||||||
if (attribute != null && config.Value.AuthorizedFetch) {
|
if (attribute != null && config.Value.AuthorizedFetch) {
|
||||||
var request = ctx.Request;
|
var request = ctx.Request;
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||||
|
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
||||||
using Iceshrimp.Backend.Controllers.Schemas;
|
using Iceshrimp.Backend.Controllers.Schemas;
|
||||||
using Iceshrimp.Backend.Core.Configuration;
|
using Iceshrimp.Backend.Core.Configuration;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
@ -24,6 +26,8 @@ public class ErrorHandlerMiddleware(IOptions<Config.SecuritySection> options, IL
|
||||||
var logger = loggerFactory.CreateLogger(type);
|
var logger = loggerFactory.CreateLogger(type);
|
||||||
var verbosity = options.Value.ExceptionVerbosity;
|
var verbosity = options.Value.ExceptionVerbosity;
|
||||||
|
|
||||||
|
var isMastodon = ctx.GetEndpoint()?.Metadata.GetMetadata<MastodonApiControllerAttribute>() != null;
|
||||||
|
|
||||||
if (e is GracefulException ce) {
|
if (e is GracefulException ce) {
|
||||||
if (ce.StatusCode == HttpStatusCode.Accepted) {
|
if (ce.StatusCode == HttpStatusCode.Accepted) {
|
||||||
ctx.Response.StatusCode = (int)ce.StatusCode;
|
ctx.Response.StatusCode = (int)ce.StatusCode;
|
||||||
|
@ -34,15 +38,23 @@ public class ErrorHandlerMiddleware(IOptions<Config.SecuritySection> options, IL
|
||||||
if (verbosity > ExceptionVerbosity.Basic && ce.OverrideBasic)
|
if (verbosity > ExceptionVerbosity.Basic && ce.OverrideBasic)
|
||||||
verbosity = ExceptionVerbosity.Basic;
|
verbosity = ExceptionVerbosity.Basic;
|
||||||
|
|
||||||
ctx.Response.StatusCode = (int)ce.StatusCode;
|
ctx.Response.StatusCode = (int)ce.StatusCode;
|
||||||
await ctx.Response.WriteAsJsonAsync(new ErrorResponse {
|
ctx.Response.Headers.RequestId = ctx.TraceIdentifier;
|
||||||
StatusCode = ctx.Response.StatusCode,
|
|
||||||
Error = verbosity >= ExceptionVerbosity.Basic ? ce.Error : ce.StatusCode.ToString(),
|
if (isMastodon)
|
||||||
Message = verbosity >= ExceptionVerbosity.Basic ? ce.Message : null,
|
await ctx.Response.WriteAsJsonAsync(new MastodonErrorResponse {
|
||||||
Details = verbosity == ExceptionVerbosity.Full ? ce.Details : null,
|
Error = verbosity >= ExceptionVerbosity.Basic ? ce.Message : ce.StatusCode.ToString(),
|
||||||
Source = verbosity == ExceptionVerbosity.Full ? type : null,
|
Description = verbosity >= ExceptionVerbosity.Basic ? ce.Details : null
|
||||||
RequestId = ctx.TraceIdentifier
|
});
|
||||||
});
|
else
|
||||||
|
await ctx.Response.WriteAsJsonAsync(new ErrorResponse {
|
||||||
|
StatusCode = ctx.Response.StatusCode,
|
||||||
|
Error = verbosity >= ExceptionVerbosity.Basic ? ce.Error : ce.StatusCode.ToString(),
|
||||||
|
Message = verbosity >= ExceptionVerbosity.Basic ? ce.Message : null,
|
||||||
|
Details = verbosity == ExceptionVerbosity.Full ? ce.Details : null,
|
||||||
|
Source = verbosity == ExceptionVerbosity.Full ? type : null,
|
||||||
|
RequestId = ctx.TraceIdentifier
|
||||||
|
});
|
||||||
|
|
||||||
if (!ce.SuppressLog) {
|
if (!ce.SuppressLog) {
|
||||||
if (ce.Details != null)
|
if (ce.Details != null)
|
||||||
|
@ -120,7 +132,8 @@ public class GracefulException(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This is intended for cases where no error occured, but the request needs to be aborted early (e.g. WebFinger returning 410 Gone)
|
/// This is intended for cases where no error occured, but the request needs to be aborted early (e.g. WebFinger
|
||||||
|
/// returning 410 Gone)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static GracefulException Accepted(string message) {
|
public static GracefulException Accepted(string message) {
|
||||||
return new GracefulException(HttpStatusCode.Accepted, message);
|
return new GracefulException(HttpStatusCode.Accepted, message);
|
||||||
|
|
|
@ -1,67 +0,0 @@
|
||||||
using Iceshrimp.Backend.Core.Database;
|
|
||||||
using Iceshrimp.Backend.Core.Database.Tables;
|
|
||||||
using Iceshrimp.Backend.Core.Helpers;
|
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Middleware;
|
|
||||||
|
|
||||||
public class OauthAuthenticationMiddleware(DatabaseContext db) : IMiddleware {
|
|
||||||
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) {
|
|
||||||
var endpoint = ctx.Features.Get<IEndpointFeature>()?.Endpoint;
|
|
||||||
var attribute = endpoint?.Metadata.GetMetadata<AuthenticateOauthAttribute>();
|
|
||||||
|
|
||||||
if (attribute != null) {
|
|
||||||
var request = ctx.Request;
|
|
||||||
var header = request.Headers.Authorization.ToString();
|
|
||||||
if (!header.ToLowerInvariant().StartsWith("bearer ")) {
|
|
||||||
await next(ctx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
header = header[7..];
|
|
||||||
var token = await db.OauthTokens
|
|
||||||
.Include(p => p.User)
|
|
||||||
.ThenInclude(p => p.UserProfile)
|
|
||||||
.Include(p => p.App)
|
|
||||||
.FirstOrDefaultAsync(p => p.Token == header && p.Active);
|
|
||||||
|
|
||||||
if (token == null) {
|
|
||||||
await next(ctx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attribute.Scopes.Length > 0 &&
|
|
||||||
attribute.Scopes.Except(MastodonOauthHelpers.ExpandScopes(token.Scopes)).Any()) {
|
|
||||||
await next(ctx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.SetOauthToken(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
await next(ctx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AuthenticateOauthAttribute(params string[] scopes) : Attribute {
|
|
||||||
public readonly string[] Scopes = scopes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static partial class HttpContextExtensions {
|
|
||||||
private const string MastodonKey = "masto-session";
|
|
||||||
|
|
||||||
internal static void SetOauthToken(this HttpContext ctx, OauthToken session) {
|
|
||||||
ctx.Items.Add(MastodonKey, session);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static OauthToken? GetOauthToken(this HttpContext ctx) {
|
|
||||||
ctx.Items.TryGetValue(MastodonKey, out var session);
|
|
||||||
return session as OauthToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static User? GetOauthUser(this HttpContext ctx) {
|
|
||||||
ctx.Items.TryGetValue(MastodonKey, out var session);
|
|
||||||
return (session as OauthToken)?.User;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
using Iceshrimp.Backend.Core.Helpers;
|
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Middleware;
|
|
||||||
|
|
||||||
public class OauthAuthorizationMiddleware : IMiddleware {
|
|
||||||
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) {
|
|
||||||
var endpoint = ctx.Features.Get<IEndpointFeature>()?.Endpoint;
|
|
||||||
var attribute = endpoint?.Metadata.GetMetadata<AuthorizeOauthAttribute>();
|
|
||||||
|
|
||||||
if (attribute != null) {
|
|
||||||
var token = ctx.GetOauthToken();
|
|
||||||
if (token is not { Active: true })
|
|
||||||
throw GracefulException.Unauthorized("This method requires an authenticated user");
|
|
||||||
if (attribute.Scopes.Length > 0 &&
|
|
||||||
attribute.Scopes.Except(MastodonOauthHelpers.ExpandScopes(token.Scopes)).Any())
|
|
||||||
throw GracefulException.Forbidden("This action is outside the authorized scopes");
|
|
||||||
}
|
|
||||||
|
|
||||||
await next(ctx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AuthorizeOauthAttribute(params string[] scopes) : Attribute {
|
|
||||||
public readonly string[] Scopes = scopes;
|
|
||||||
}
|
|
|
@ -1,14 +1,9 @@
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Middleware;
|
namespace Iceshrimp.Backend.Core.Middleware;
|
||||||
|
|
||||||
public class RequestBufferingMiddleware : IMiddleware {
|
public class RequestBufferingMiddleware : IMiddleware {
|
||||||
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) {
|
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) {
|
||||||
var endpoint = ctx.Features.Get<IEndpointFeature>()?.Endpoint;
|
var attribute = ctx.GetEndpoint()?.Metadata.GetMetadata<EnableRequestBufferingAttribute>();
|
||||||
var attribute = endpoint?.Metadata.GetMetadata<EnableRequestBufferingAttribute>();
|
|
||||||
|
|
||||||
if (attribute != null) ctx.Request.EnableBuffering(attribute.MaxLength);
|
if (attribute != null) ctx.Request.EnableBuffering(attribute.MaxLength);
|
||||||
|
|
||||||
await next(ctx);
|
await next(ctx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue