using System.IdentityModel.Tokens.Jwt; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using SIGCM2.Application.Abstractions.Persistence; using SIGCM2.Application.Audit; using SIGCM2.Application.Common; namespace SIGCM2.Api.Authorization; /// /// Authorization handler for . /// UDT-009: Reads "rol" + "sub" claims, queries both IRolPermisoRepository /// and IUsuarioRepository, resolves effective permissions via PermisoResolver, /// and succeeds if at least one required permission matches (OR semantics). /// No caching — always authoritative from DB (UDT-006 D1, UDT-009 D3). /// UDT-010: emits SecurityEvent 'permission.denied' on rejection. /// public sealed class PermissionAuthorizationHandler : AuthorizationHandler { private readonly IRolPermisoRepository _rolPermisoRepo; private readonly IUsuarioRepository _usuarioRepo; private readonly ISecurityEventLogger _security; private readonly ILogger _logger; public PermissionAuthorizationHandler( IRolPermisoRepository rolPermisoRepo, IUsuarioRepository usuarioRepo, ISecurityEventLogger security, ILogger logger) { _rolPermisoRepo = rolPermisoRepo; _usuarioRepo = usuarioRepo; _security = security; _logger = logger; } protected override async Task HandleRequirementAsync( AuthorizationHandlerContext context, RequirePermissionAttribute requirement) { // 1. Must be authenticated — defense-in-depth if (context.User?.Identity?.IsAuthenticated != true) return; // implicit Fail // 2. Extract "rol" claim var rolCodigo = context.User.FindFirst("rol")?.Value; if (string.IsNullOrWhiteSpace(rolCodigo)) { _logger.LogWarning( "Authorization failed — token missing 'rol' claim for user {User}", context.User.Identity?.Name); context.Fail(new AuthorizationFailureReason(this, "missing_rol_claim")); return; } // 3. Extract "sub" claim — MapInboundClaims=false so it stays as "sub" (NOT NameIdentifier) var subClaim = context.User.FindFirst(JwtRegisteredClaimNames.Sub)?.Value ?? context.User.FindFirst("sub")?.Value; if (string.IsNullOrWhiteSpace(subClaim) || !int.TryParse(subClaim, out var userId)) { _logger.LogWarning( "Authorization failed — token missing or non-numeric 'sub' claim for user {User}", context.User.Identity?.Name); context.Fail(new AuthorizationFailureReason(this, "missing_sub_claim")); return; } // 4. Load role permissions — no cache (UDT-006 D1) var rolPermisoEntities = await _rolPermisoRepo.GetByRolCodigoAsync(rolCodigo); var rolPermisos = rolPermisoEntities.Select(p => p.Codigo); // 5. Load user overrides — no cache (UDT-009 D3); null usuario → no overrides var usuario = await _usuarioRepo.GetByIdAsync(userId); var overrides = PermisosOverride.FromJson(usuario?.PermisosJson); // 6. Resolve effective permissions var effective = PermisoResolver.Resolve(rolPermisos, overrides); // 7. OR semantics — any single match is enough var matched = requirement.PermissionCodes.FirstOrDefault(effective.Contains); if (matched is not null) { context.Succeed(requirement); return; } // 8. Stash required permission for ForbiddenProblemDetailsHandler var requiredPermission = requirement.PermissionCodes[0]; if (context.Resource is HttpContext httpContext) httpContext.Items["RequiredPermission"] = requiredPermission; // 9. Emit SecurityEvent for the denial var endpoint = (context.Resource as HttpContext)?.Request?.Path.Value; var method = (context.Resource as HttpContext)?.Request?.Method; await _security.LogAsync("permission.denied", "failure", actorUserId: userId, failureReason: $"missing_permission:{requiredPermission}", metadata: new { permissionRequired = requiredPermission, endpoint, method }); context.Fail(new AuthorizationFailureReason(this, $"missing_permission:{string.Join('|', requirement.PermissionCodes)}")); } }