[backend/core] Add 401/403 response examples programmatically
This commit is contained in:
parent
72bc5e1090
commit
ba0e041bad
12 changed files with 83 additions and 55 deletions
|
@ -15,8 +15,6 @@ namespace Iceshrimp.Backend.Controllers;
|
|||
[Authorize("role:admin")]
|
||||
[ApiController]
|
||||
[Route("/api/v1/iceshrimp/admin")]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(ErrorResponse))]
|
||||
[SuppressMessage("ReSharper", "SuggestBaseTypeForParameterInConstructor",
|
||||
Justification = "We only have a DatabaseContext in our DI pool, not the base type")]
|
||||
public class AdminController(DatabaseContext db, ActivityPubController apController) : ControllerBase
|
||||
|
|
|
@ -115,7 +115,6 @@ public class AuthController(DatabaseContext db, UserService userSvc) : Controlle
|
|||
[Consumes(MediaTypeNames.Application.Json)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AuthResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(ErrorResponse))]
|
||||
[SuppressMessage("ReSharper.DPA", "DPA0011: High execution time of MVC action",
|
||||
Justification = "Argon2 is execution time-heavy by design")]
|
||||
public async Task<IActionResult> ChangePassword([FromBody] ChangePasswordRequest request)
|
||||
|
@ -124,7 +123,7 @@ public class AuthController(DatabaseContext db, UserService userSvc) : Controlle
|
|||
var userProfile = await db.UserProfiles.FirstOrDefaultAsync(p => p.User == user);
|
||||
if (userProfile is not { Password: not null }) throw new GracefulException("userProfile?.Password was null");
|
||||
if (!AuthHelpers.ComparePassword(request.OldPassword, userProfile.Password))
|
||||
throw GracefulException.Forbidden("old_password is invalid");
|
||||
throw GracefulException.BadRequest("old_password is invalid");
|
||||
|
||||
userProfile.Password = AuthHelpers.HashPassword(request.NewPassword);
|
||||
await db.SaveChangesAsync();
|
||||
|
|
|
@ -33,8 +33,6 @@ public class AccountController(
|
|||
[HttpGet("verify_credentials")]
|
||||
[Authorize("read:accounts")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AccountEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> VerifyUserCredentials()
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
|
@ -56,8 +54,6 @@ public class AccountController(
|
|||
[HttpPost("{id}/follow")]
|
||||
[Authorize("write:follows")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(RelationshipEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))]
|
||||
//TODO: [FromHybrid] request (bool reblogs, bool notify, bool languages)
|
||||
public async Task<IActionResult> FollowUser(string id)
|
||||
{
|
||||
|
@ -108,8 +104,6 @@ public class AccountController(
|
|||
[HttpPost("{id}/unfollow")]
|
||||
[Authorize("write:follows")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(RelationshipEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> UnfollowUser(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
|
@ -149,8 +143,6 @@ public class AccountController(
|
|||
[HttpGet("relationships")]
|
||||
[Authorize("read:follows")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(RelationshipEntity[]))]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> GetRelationships([FromQuery(Name = "id")] List<string> ids)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
|
@ -186,8 +178,6 @@ public class AccountController(
|
|||
[Authorize("read:statuses")]
|
||||
[LinkPagination(20, 40)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<StatusEntity>))]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> GetUserStatuses(
|
||||
string id, AccountSchemas.AccountStatusesRequest request, MastodonPaginationQuery query
|
||||
)
|
||||
|
@ -275,8 +265,6 @@ public class AccountController(
|
|||
[Authorize("read:follows")]
|
||||
[LinkPagination(40, 80)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<AccountEntity>))]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> GetFollowRequests(MastodonPaginationQuery query)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
|
@ -293,8 +281,6 @@ public class AccountController(
|
|||
[HttpPost("/api/v1/follow_requests/{id}/authorize")]
|
||||
[Authorize("write:follows")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(RelationshipEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> AcceptFollowRequest(string id)
|
||||
{
|
||||
|
@ -338,8 +324,6 @@ public class AccountController(
|
|||
[HttpPost("/api/v1/follow_requests/{id}/reject")]
|
||||
[Authorize("write:follows")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(RelationshipEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> RejectFollowRequest(string id)
|
||||
{
|
||||
|
|
|
@ -23,8 +23,6 @@ namespace Iceshrimp.Backend.Controllers.Mastodon;
|
|||
[EnableRateLimiting("sliding")]
|
||||
[EnableCors("mastodon")]
|
||||
[Produces(MediaTypeNames.Application.Json)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))]
|
||||
public class ListController(DatabaseContext db, UserRenderer userRenderer) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
|
|
|
@ -17,8 +17,6 @@ namespace Iceshrimp.Backend.Controllers.Mastodon;
|
|||
[EnableRateLimiting("sliding")]
|
||||
[Produces(MediaTypeNames.Application.Json)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AttachmentEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))]
|
||||
public class MediaController(DriveService driveSvc) : ControllerBase
|
||||
{
|
||||
[HttpPost("/api/v1/media")]
|
||||
|
|
|
@ -85,8 +85,6 @@ public class StatusController(
|
|||
[HttpPost("{id}/favourite")]
|
||||
[Authorize("write:favourites")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(StatusEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> LikeNote(string id)
|
||||
{
|
||||
|
@ -104,8 +102,6 @@ public class StatusController(
|
|||
[HttpPost("{id}/unfavourite")]
|
||||
[Authorize("write:favourites")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(StatusEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> UnlikeNote(string id)
|
||||
{
|
||||
|
@ -123,8 +119,6 @@ public class StatusController(
|
|||
[HttpPost("{id}/reblog")]
|
||||
[Authorize("write:favourites")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(StatusEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> Renote(string id, [FromHybrid] string? visibility)
|
||||
{
|
||||
|
@ -153,8 +147,6 @@ public class StatusController(
|
|||
[HttpPost("{id}/unreblog")]
|
||||
[Authorize("write:favourites")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(StatusEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> UndoRenote(string id)
|
||||
{
|
||||
|
|
|
@ -23,8 +23,6 @@ namespace Iceshrimp.Backend.Controllers.Mastodon;
|
|||
[EnableRateLimiting("sliding")]
|
||||
[EnableCors("mastodon")]
|
||||
[Produces(MediaTypeNames.Application.Json)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))]
|
||||
public class TimelineController(DatabaseContext db, NoteRenderer noteRenderer, IDistributedCache cache) : ControllerBase
|
||||
{
|
||||
[Authorize("read:statuses")]
|
||||
|
|
|
@ -41,8 +41,6 @@ public class NoteController(DatabaseContext db, NoteService noteSvc, NoteRendere
|
|||
[Authorize]
|
||||
[Consumes(MediaTypeNames.Application.Json)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(NoteResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> CreateNote(NoteCreateRequest request)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
|
|
|
@ -23,8 +23,6 @@ public class TimelineController(DatabaseContext db, IDistributedCache cache, Not
|
|||
[Authenticate]
|
||||
[Authorize]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<NoteResponse>))]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> GetHomeTimeline(PaginationQuery pq)
|
||||
{
|
||||
|
|
|
@ -147,14 +147,6 @@ public static class ServiceExtensions
|
|||
Type = SecuritySchemeType.Http,
|
||||
Scheme = "bearer"
|
||||
});
|
||||
options.AddSecurityDefinition("admin",
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Name = "Authorization token",
|
||||
In = ParameterLocation.Header,
|
||||
Type = SecuritySchemeType.Http,
|
||||
Scheme = "bearer"
|
||||
});
|
||||
options.AddSecurityDefinition("mastodon",
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.Reflection;
|
|||
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||
using Iceshrimp.Backend.Core.Middleware;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
|
@ -45,19 +46,25 @@ public static class SwaggerGenOptionsExtensions
|
|||
if (context.MethodInfo.DeclaringType is null)
|
||||
return;
|
||||
|
||||
//TODO: separate admin & user authorize attributes
|
||||
var hasAuthenticate = context.MethodInfo.DeclaringType.GetCustomAttributes(true)
|
||||
var authenticateAttribute = context.MethodInfo.GetCustomAttributes(true)
|
||||
.OfType<AuthenticateAttribute>()
|
||||
.Any() ||
|
||||
context.MethodInfo.GetCustomAttributes(true)
|
||||
.FirstOrDefault() ??
|
||||
context.MethodInfo.DeclaringType.GetCustomAttributes(true)
|
||||
.OfType<AuthenticateAttribute>()
|
||||
.Any();
|
||||
.FirstOrDefault();
|
||||
|
||||
if (authenticateAttribute == null) return;
|
||||
|
||||
var isMastodonController = context.MethodInfo.DeclaringType.GetCustomAttributes(true)
|
||||
.OfType<MastodonApiControllerAttribute>()
|
||||
.Any();
|
||||
|
||||
if (!hasAuthenticate) return;
|
||||
var authorizeAttribute = context.MethodInfo.GetCustomAttributes(true)
|
||||
.OfType<AuthorizeAttribute>()
|
||||
.FirstOrDefault() ??
|
||||
context.MethodInfo.DeclaringType.GetCustomAttributes(true)
|
||||
.OfType<AuthorizeAttribute>()
|
||||
.FirstOrDefault();
|
||||
|
||||
var schema = new OpenApiSecurityScheme
|
||||
{
|
||||
|
@ -68,6 +75,72 @@ public static class SwaggerGenOptionsExtensions
|
|||
};
|
||||
|
||||
operation.Security = new List<OpenApiSecurityRequirement> { new() { [schema] = Array.Empty<string>() } };
|
||||
|
||||
if (authorizeAttribute == null) return;
|
||||
|
||||
const string web401 =
|
||||
"""
|
||||
{
|
||||
"statusCode": 401,
|
||||
"error": "Unauthorized",
|
||||
"message": "This method requires an authenticated user"
|
||||
}
|
||||
""";
|
||||
|
||||
const string web403 =
|
||||
"""
|
||||
{
|
||||
"statusCode": 403,
|
||||
"error": "Forbidden",
|
||||
"message": "This action is outside the authorized scopes"
|
||||
}
|
||||
""";
|
||||
|
||||
const string masto401 =
|
||||
"""
|
||||
{
|
||||
"error": "This method requires an authenticated user"
|
||||
}
|
||||
""";
|
||||
|
||||
const string masto403 =
|
||||
"""
|
||||
{
|
||||
"message": "This action is outside the authorized scopes"
|
||||
}
|
||||
""";
|
||||
|
||||
var example401 = new OpenApiString(isMastodonController ? masto401 : web401);
|
||||
|
||||
var res401 = new OpenApiResponse
|
||||
{
|
||||
Description = "Unauthorized",
|
||||
Content = new Dictionary<string, OpenApiMediaType>
|
||||
{
|
||||
{ "application/json", new OpenApiMediaType { Example = example401 } }
|
||||
}
|
||||
};
|
||||
|
||||
operation.Responses.Remove("401");
|
||||
operation.Responses.Add("401", res401);
|
||||
|
||||
if (authorizeAttribute is { AdminRole: false, ModeratorRole: false, Scopes.Length: 0 } &&
|
||||
authenticateAttribute is { AdminRole: false, ModeratorRole: false, Scopes.Length: 0 })
|
||||
return;
|
||||
|
||||
operation.Responses.Remove("403");
|
||||
|
||||
var example403 = new OpenApiString(isMastodonController ? masto403 : web403);
|
||||
|
||||
var res403 = new OpenApiResponse
|
||||
{
|
||||
Description = "Forbidden",
|
||||
Content = new Dictionary<string, OpenApiMediaType>
|
||||
{
|
||||
{ "application/json", new OpenApiMediaType { Example = example403 } }
|
||||
}
|
||||
};
|
||||
operation.Responses.Add("403", res403);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ public class AuthorizationMiddleware : IMiddleware
|
|||
{
|
||||
var session = ctx.GetSession();
|
||||
if (session is not { Active: true })
|
||||
throw GracefulException.Forbidden("This method requires an authenticated user");
|
||||
throw GracefulException.Unauthorized("This method requires an authenticated user");
|
||||
if (attribute.AdminRole && !session.User.IsAdmin)
|
||||
throw GracefulException.Forbidden("This action is outside the authorized scopes");
|
||||
if (attribute.ModeratorRole && session.User is { IsAdmin: false, IsModerator: false })
|
||||
|
|
Loading…
Add table
Reference in a new issue