UDT-009: Overrides de PermisosJson por usuario — cierre módulo Auth #12
@@ -246,6 +246,30 @@ public sealed class UsuariosController : ControllerBase
|
|||||||
return Ok(MapToPermisosResponse(result));
|
return Ok(MapToPermisosResponse(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Replaces the grant/deny override sets for a usuario.
|
||||||
|
/// Requires administracion:usuarios:gestionar.
|
||||||
|
/// </summary>
|
||||||
|
[HttpPut("{id:int}/permisos/overrides")]
|
||||||
|
[RequirePermission("administracion:usuarios:gestionar")]
|
||||||
|
[ProducesResponseType(typeof(UsuarioPermisosResponse), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<IActionResult> UpdatePermisosOverrides(
|
||||||
|
[FromRoute] int id,
|
||||||
|
[FromBody] UpdatePermisosOverridesRequest request)
|
||||||
|
{
|
||||||
|
var command = new UpdateUsuarioPermisosOverridesCommand(
|
||||||
|
Id: id,
|
||||||
|
Grant: request.Grant ?? [],
|
||||||
|
Deny: request.Deny ?? []);
|
||||||
|
|
||||||
|
var result = await _dispatcher.Send<UpdateUsuarioPermisosOverridesCommand, UsuarioPermisosDto>(command);
|
||||||
|
return Ok(MapToPermisosResponse(result));
|
||||||
|
}
|
||||||
|
|
||||||
private static UsuarioPermisosResponse MapToPermisosResponse(UsuarioPermisosDto dto)
|
private static UsuarioPermisosResponse MapToPermisosResponse(UsuarioPermisosDto dto)
|
||||||
=> new(
|
=> new(
|
||||||
RolPermisos: dto.RolPermisos,
|
RolPermisos: dto.RolPermisos,
|
||||||
@@ -266,6 +290,11 @@ public sealed record PermisosOverridesShape(
|
|||||||
IReadOnlyList<string> Grant,
|
IReadOnlyList<string> Grant,
|
||||||
IReadOnlyList<string> Deny);
|
IReadOnlyList<string> Deny);
|
||||||
|
|
||||||
|
/// <summary>UDT-009: PUT permisos/overrides request body.</summary>
|
||||||
|
public sealed record UpdatePermisosOverridesRequest(
|
||||||
|
IReadOnlyList<string>? Grant,
|
||||||
|
IReadOnlyList<string>? Deny);
|
||||||
|
|
||||||
/// <summary>Create user request body — nullable to catch missing field scenarios.</summary>
|
/// <summary>Create user request body — nullable to catch missing field scenarios.</summary>
|
||||||
public sealed record CreateUsuarioRequest(
|
public sealed record CreateUsuarioRequest(
|
||||||
string? Username,
|
string? Username,
|
||||||
|
|||||||
@@ -169,6 +169,35 @@ public sealed class ExceptionFilter : IExceptionFilter
|
|||||||
context.ExceptionHandled = true;
|
context.ExceptionHandled = true;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// UDT-009: permiso override validation errors
|
||||||
|
case InvalidPermisoCodesException ipce:
|
||||||
|
context.Result = new ObjectResult(new Microsoft.AspNetCore.Mvc.ProblemDetails
|
||||||
|
{
|
||||||
|
Type = "about:blank",
|
||||||
|
Title = "invalid-permiso-codes",
|
||||||
|
Status = 400,
|
||||||
|
Extensions = { ["invalidCodes"] = ipce.InvalidCodes }
|
||||||
|
})
|
||||||
|
{
|
||||||
|
StatusCode = StatusCodes.Status400BadRequest
|
||||||
|
};
|
||||||
|
context.ExceptionHandled = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GrantDenyOverlapException gdoe:
|
||||||
|
context.Result = new ObjectResult(new Microsoft.AspNetCore.Mvc.ProblemDetails
|
||||||
|
{
|
||||||
|
Type = "about:blank",
|
||||||
|
Title = "grant-deny-overlap",
|
||||||
|
Status = 400,
|
||||||
|
Extensions = { ["overlap"] = gdoe.Overlap }
|
||||||
|
})
|
||||||
|
{
|
||||||
|
StatusCode = StatusCodes.Status400BadRequest
|
||||||
|
};
|
||||||
|
context.ExceptionHandled = true;
|
||||||
|
break;
|
||||||
|
|
||||||
case ValidationException validationEx:
|
case ValidationException validationEx:
|
||||||
var errors = validationEx.Errors
|
var errors = validationEx.Errors
|
||||||
.GroupBy(e => e.PropertyName)
|
.GroupBy(e => e.PropertyName)
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ public static class DependencyInjection
|
|||||||
|
|
||||||
// Usuarios/Permisos (UDT-009)
|
// Usuarios/Permisos (UDT-009)
|
||||||
services.AddScoped<ICommandHandler<GetUsuarioPermisosQuery, UsuarioPermisosDto>, GetUsuarioPermisosQueryHandler>();
|
services.AddScoped<ICommandHandler<GetUsuarioPermisosQuery, UsuarioPermisosDto>, GetUsuarioPermisosQueryHandler>();
|
||||||
|
services.AddScoped<ICommandHandler<UpdateUsuarioPermisosOverridesCommand, UsuarioPermisosDto>, UpdateUsuarioPermisosOverridesCommandHandler>();
|
||||||
|
|
||||||
// FluentValidation validators (scans entire Application assembly)
|
// FluentValidation validators (scans entire Application assembly)
|
||||||
services.AddValidatorsFromAssemblyContaining<LoginCommandValidator>();
|
services.AddValidatorsFromAssemblyContaining<LoginCommandValidator>();
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace SIGCM2.Application.Usuarios.Permisos;
|
||||||
|
|
||||||
|
/// <summary>UDT-009: Command to replace the grant/deny override sets for a usuario.</summary>
|
||||||
|
public sealed record UpdateUsuarioPermisosOverridesCommand(
|
||||||
|
int Id,
|
||||||
|
IReadOnlyList<string> Grant,
|
||||||
|
IReadOnlyList<string> Deny);
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
using SIGCM2.Application.Abstractions;
|
||||||
|
using SIGCM2.Application.Abstractions.Persistence;
|
||||||
|
using SIGCM2.Application.Common;
|
||||||
|
using SIGCM2.Domain.Exceptions;
|
||||||
|
|
||||||
|
namespace SIGCM2.Application.Usuarios.Permisos;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// UDT-009: Handles PUT /api/v1/users/{id}/permisos/overrides.
|
||||||
|
/// Validates overlap and catalog existence, persists new overrides, returns updated effective set.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UpdateUsuarioPermisosOverridesCommandHandler
|
||||||
|
: ICommandHandler<UpdateUsuarioPermisosOverridesCommand, UsuarioPermisosDto>
|
||||||
|
{
|
||||||
|
private readonly IUsuarioRepository _usuarioRepo;
|
||||||
|
private readonly IRolPermisoRepository _rolPermisoRepo;
|
||||||
|
private readonly IPermisoRepository _permisoRepo;
|
||||||
|
|
||||||
|
public UpdateUsuarioPermisosOverridesCommandHandler(
|
||||||
|
IUsuarioRepository usuarioRepo,
|
||||||
|
IRolPermisoRepository rolPermisoRepo,
|
||||||
|
IPermisoRepository permisoRepo)
|
||||||
|
{
|
||||||
|
_usuarioRepo = usuarioRepo;
|
||||||
|
_rolPermisoRepo = rolPermisoRepo;
|
||||||
|
_permisoRepo = permisoRepo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UsuarioPermisosDto> Handle(UpdateUsuarioPermisosOverridesCommand command)
|
||||||
|
{
|
||||||
|
var grant = command.Grant ?? [];
|
||||||
|
var deny = command.Deny ?? [];
|
||||||
|
|
||||||
|
// 1. Overlap check — grant ∩ deny → 400
|
||||||
|
var overlap = grant.Intersect(deny, StringComparer.Ordinal).ToArray();
|
||||||
|
if (overlap.Length > 0)
|
||||||
|
throw new GrantDenyOverlapException(overlap);
|
||||||
|
|
||||||
|
// 2. Catalog existence check
|
||||||
|
var allCodes = grant.Concat(deny).Distinct(StringComparer.Ordinal).ToArray();
|
||||||
|
if (allCodes.Length > 0)
|
||||||
|
{
|
||||||
|
var existentes = await _permisoRepo.GetByCodigosAsync(allCodes);
|
||||||
|
var existSet = existentes.Select(p => p.Codigo).ToHashSet(StringComparer.Ordinal);
|
||||||
|
var faltantes = allCodes.Where(c => !existSet.Contains(c)).ToArray();
|
||||||
|
if (faltantes.Length > 0)
|
||||||
|
throw new InvalidPermisoCodesException(faltantes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Load usuario
|
||||||
|
var usuario = await _usuarioRepo.GetByIdAsync(command.Id)
|
||||||
|
?? throw new UsuarioNotFoundException(command.Id);
|
||||||
|
|
||||||
|
// 4. Persist — use WithPermisosJson to get updated FechaModificacion
|
||||||
|
var newOverrides = new PermisosOverride(grant, deny);
|
||||||
|
var updated = usuario.WithPermisosJson(newOverrides.ToJson());
|
||||||
|
await _usuarioRepo.UpdatePermisosJsonAsync(
|
||||||
|
updated.Id,
|
||||||
|
updated.PermisosJson,
|
||||||
|
updated.FechaModificacion!.Value);
|
||||||
|
|
||||||
|
// 5. Return updated effective set
|
||||||
|
var rolPermisoEntities = await _rolPermisoRepo.GetByRolCodigoAsync(updated.Rol);
|
||||||
|
var rolPermisos = rolPermisoEntities
|
||||||
|
.Select(p => p.Codigo)
|
||||||
|
.OrderBy(c => c, StringComparer.Ordinal)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var effective = PermisoResolver.Resolve(rolPermisos, newOverrides)
|
||||||
|
.OrderBy(c => c, StringComparer.Ordinal)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return new UsuarioPermisosDto(
|
||||||
|
UsuarioId: updated.Id,
|
||||||
|
Rol: updated.Rol,
|
||||||
|
RolPermisos: rolPermisos,
|
||||||
|
Grant: grant,
|
||||||
|
Deny: deny,
|
||||||
|
Effective: effective);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
namespace SIGCM2.Domain.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// UDT-009: Thrown when the same code appears in both grant and deny arrays.
|
||||||
|
/// Maps to 400 { title: "grant-deny-overlap", overlap: [...] }.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GrantDenyOverlapException : Exception
|
||||||
|
{
|
||||||
|
public IReadOnlyList<string> Overlap { get; }
|
||||||
|
|
||||||
|
public GrantDenyOverlapException(IReadOnlyList<string> overlap)
|
||||||
|
: base($"Los siguientes códigos aparecen en grant y deny simultáneamente: {string.Join(", ", overlap)}")
|
||||||
|
{
|
||||||
|
Overlap = overlap;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
namespace SIGCM2.Domain.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// UDT-009: Thrown when grant or deny arrays contain codes not in the Permiso catalog.
|
||||||
|
/// Maps to 400 { title: "invalid-permiso-codes", invalidCodes: [...] }.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class InvalidPermisoCodesException : Exception
|
||||||
|
{
|
||||||
|
public IReadOnlyList<string> InvalidCodes { get; }
|
||||||
|
|
||||||
|
public InvalidPermisoCodesException(IReadOnlyList<string> codes)
|
||||||
|
: base($"Códigos de permiso inexistentes en el catálogo: {string.Join(", ", codes)}")
|
||||||
|
{
|
||||||
|
InvalidCodes = codes;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user