UDT-006: Middleware de Autorización (RBAC enforcement) #10

Merged
dmolinari merged 9 commits from feature/UDT-006 into main 2026-04-15 20:15:18 +00:00
2 changed files with 59 additions and 0 deletions
Showing only changes of commit 4866c4f21f - Show all commits

View File

@@ -0,0 +1,58 @@
using System.Text.Json;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Policy;
namespace SIGCM2.Api.Authorization;
/// <summary>
/// Custom IAuthorizationMiddlewareResultHandler that emits a structured ProblemDetails
/// response for 403 Forbidden outcomes (authenticated user, missing permission).
///
/// For 401 Unauthorized and successful outcomes, delegates to the default handler
/// so the existing JWT Bearer challenge flow is unaffected (REQ-B-07).
///
/// Registered as singleton in Program.cs — depends only on framework services.
/// </summary>
public sealed class ForbiddenProblemDetailsHandler : IAuthorizationMiddlewareResultHandler
{
private static readonly AuthorizationMiddlewareResultHandler DefaultHandler = new();
private static readonly JsonSerializerOptions SerializerOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
public async Task HandleAsync(
RequestDelegate next,
HttpContext context,
AuthorizationPolicy policy,
PolicyAuthorizationResult authorizeResult)
{
// Only intercept 403s for authenticated users.
// If the user is not authenticated, the 401 challenge is handled by JwtBearer (REQ-B-07).
if (authorizeResult.Forbidden && context.User.Identity?.IsAuthenticated == true)
{
var requiredPermission = context.Items["RequiredPermission"] as string;
context.Response.StatusCode = StatusCodes.Status403Forbidden;
context.Response.ContentType = "application/problem+json; charset=utf-8";
var problem = new
{
type = "https://sigcm2.local/errors/forbidden",
title = "Acceso denegado",
status = 403,
detail = "No tenés el permiso requerido para ejecutar esta acción.",
permisoRequerido = requiredPermission,
};
await context.Response.WriteAsync(
JsonSerializer.Serialize(problem, SerializerOptions));
return;
}
// Delegate 401 challenges and successful outcomes to the default handler
await DefaultHandler.HandleAsync(next, context, policy, authorizeResult);
}
}

View File

@@ -26,6 +26,7 @@ builder.Services.AddInfrastructure(builder.Configuration);
// Authorization — handler lives in Api layer; DO NOT move to Infrastructure DI
builder.Services.AddAuthorization();
builder.Services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();
builder.Services.AddSingleton<IAuthorizationMiddlewareResultHandler, ForbiddenProblemDetailsHandler>();
// Controllers with exception filter
builder.Services.AddControllers(opts =>