2026-04-15 16:26:30 -03:00
|
|
|
using Microsoft.AspNetCore.Authorization;
|
2026-04-13 21:36:08 -03:00
|
|
|
using Serilog;
|
|
|
|
|
using Scalar.AspNetCore;
|
2026-04-15 16:26:30 -03:00
|
|
|
using SIGCM2.Api.Authorization;
|
feat(api): GET /audit/events + /health/audit (UDT-010 B10)
AuditController:
- GET /api/v1/audit/events?actorUserId&targetType&targetId&from&to&cursor&limit
- Protected by [RequirePermission("administracion:auditoria:ver")] — reuses
the existing permission (V005/V006 seed assigns it to admin).
- 400 on limit out of [1,100] or from > to.
- Cursor-based DESC pagination via AuditEventRepository.QueryAsync.
AuditHealthCheck (IHealthCheck):
- Validates SYSTEM_VERSIONING ON on Usuario/Rol/Permiso/RolPermiso.
- Validates partition boundaries exist for next 3 months (both AuditEvent and
SecurityEvent functions).
- Reports last audit event age (lenient 24h to accommodate dev/test quiet envs).
- Validates HISTORY_RETENTION_PERIOD == 10 YEARS on all 4 tables.
Key fix during impl: sys.tables.history_retention_period is stored in UNITS
(1=INFINITE, 3=DAY, 4=WEEK, 5=MONTH, 6=YEAR), NOT seconds. Assertion: period=10
AND unit=6 (10 YEARS).
- Mapped at /health/audit via app.MapHealthChecks with tag 'audit'.
Tests (Strict TDD, integration against SIGCM2_Test):
- AuditControllerTests (5): without-auth 401, without-permission 403 (cajero),
admin with filter returns events, invalid limit 400, from>to 400.
- AuditHealthCheckTests (1): returns Healthy with V010 applied.
Suite: 378/378 Application.Tests + 147/147 Api.Tests = 525/525 passing.
Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-AUD-7/8, design, tasks#B10}
2026-04-16 17:05:40 -03:00
|
|
|
using SIGCM2.Api.HealthChecks;
|
feat(api): audit context middleware + scoped impl (UDT-010 B4)
Wires the request-scoped audit context per design #D-2:
Middleware pipeline in Program.cs:
app.UseCors()
app.UseMiddleware<CorrelationIdMiddleware>() // PRE-AUTH
app.UseAuthentication()
app.UseMiddleware<AuditActorMiddleware>() // POST-AUTH
app.UseAuthorization()
app.MapControllers()
SIGCM2.Api/Middleware/CorrelationIdMiddleware.cs:
- Preserves client-sent X-Correlation-Id header when a valid GUID, otherwise
generates Guid.NewGuid(). Stores in HttpContext.Items (audit:correlationId).
- Captures Ip (Connection.RemoteIpAddress) + UserAgent header into Items.
- Echoes the correlation id back via response header (OnStarting + immediate
set — immediate set makes unit testing against DefaultHttpContext reliable).
SIGCM2.Api/Middleware/AuditActorMiddleware.cs:
- Reads JWT 'sub' claim from authenticated HttpContext.User, parses to int,
stores as audit:actorUserId. Anonymous / non-numeric sub leaves it unset.
SIGCM2.Infrastructure/Audit/AuditContext.cs (IAuditContext scoped impl):
- Reads Items entries via IHttpContextAccessor. Returns null / Guid.Empty
when no HttpContext is available (jobs, tests without middleware).
- ActorRoleId intentionally null for now — rol code → id resolution is
deferred; the logger may resolve it at persist time in a later batch.
DI registration (Infrastructure/DependencyInjection.cs):
- services.AddScoped<IAuditContext, AuditContext>()
Tests (Strict TDD):
- CorrelationIdMiddlewareTests (6): generates/preserves/handles-malformed
correlation id, sets response header, captures ip/ua, calls next.
- AuditActorMiddlewareTests (5): authenticated/anonymous/no-sub/non-numeric/
calls-next.
- AuditContextTests (7): reads from Items, null-http-context defaults,
ActorRoleId currently null.
Suite: 355/355 Application.Tests + 141/141 Api.Tests = 496/496 passing.
Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-AUD-3/9, design#D-2, tasks#B4}
2026-04-16 13:32:13 -03:00
|
|
|
using SIGCM2.Api.Middleware;
|
2026-04-13 21:36:08 -03:00
|
|
|
using SIGCM2.Application;
|
|
|
|
|
using SIGCM2.Infrastructure;
|
|
|
|
|
using SIGCM2.Api.Filters;
|
|
|
|
|
|
|
|
|
|
// Bootstrap logger — before DI is built
|
|
|
|
|
Log.Logger = new LoggerConfiguration()
|
|
|
|
|
.WriteTo.Console()
|
|
|
|
|
.CreateBootstrapLogger();
|
|
|
|
|
|
|
|
|
|
Log.Information("Starting SIGCM2 API");
|
|
|
|
|
|
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
|
|
|
|
|
|
// Serilog — reads from appsettings.json "Serilog" section
|
|
|
|
|
builder.Host.UseSerilog((ctx, lc) => lc
|
|
|
|
|
.ReadFrom.Configuration(ctx.Configuration));
|
|
|
|
|
|
|
|
|
|
// Application + Infrastructure DI
|
|
|
|
|
builder.Services.AddApplication();
|
|
|
|
|
builder.Services.AddInfrastructure(builder.Configuration);
|
|
|
|
|
|
2026-04-15 16:26:30 -03:00
|
|
|
// Authorization — handler lives in Api layer; DO NOT move to Infrastructure DI
|
|
|
|
|
builder.Services.AddAuthorization();
|
|
|
|
|
builder.Services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();
|
2026-04-15 16:27:36 -03:00
|
|
|
builder.Services.AddSingleton<IAuthorizationMiddlewareResultHandler, ForbiddenProblemDetailsHandler>();
|
2026-04-15 16:26:30 -03:00
|
|
|
|
2026-04-13 21:36:08 -03:00
|
|
|
// Controllers with exception filter
|
|
|
|
|
builder.Services.AddControllers(opts =>
|
|
|
|
|
{
|
|
|
|
|
opts.Filters.Add<ExceptionFilter>();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// OpenAPI / Scalar
|
|
|
|
|
builder.Services.AddOpenApi();
|
|
|
|
|
|
feat(api): GET /audit/events + /health/audit (UDT-010 B10)
AuditController:
- GET /api/v1/audit/events?actorUserId&targetType&targetId&from&to&cursor&limit
- Protected by [RequirePermission("administracion:auditoria:ver")] — reuses
the existing permission (V005/V006 seed assigns it to admin).
- 400 on limit out of [1,100] or from > to.
- Cursor-based DESC pagination via AuditEventRepository.QueryAsync.
AuditHealthCheck (IHealthCheck):
- Validates SYSTEM_VERSIONING ON on Usuario/Rol/Permiso/RolPermiso.
- Validates partition boundaries exist for next 3 months (both AuditEvent and
SecurityEvent functions).
- Reports last audit event age (lenient 24h to accommodate dev/test quiet envs).
- Validates HISTORY_RETENTION_PERIOD == 10 YEARS on all 4 tables.
Key fix during impl: sys.tables.history_retention_period is stored in UNITS
(1=INFINITE, 3=DAY, 4=WEEK, 5=MONTH, 6=YEAR), NOT seconds. Assertion: period=10
AND unit=6 (10 YEARS).
- Mapped at /health/audit via app.MapHealthChecks with tag 'audit'.
Tests (Strict TDD, integration against SIGCM2_Test):
- AuditControllerTests (5): without-auth 401, without-permission 403 (cajero),
admin with filter returns events, invalid limit 400, from>to 400.
- AuditHealthCheckTests (1): returns Healthy with V010 applied.
Suite: 378/378 Application.Tests + 147/147 Api.Tests = 525/525 passing.
Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-AUD-7/8, design, tasks#B10}
2026-04-16 17:05:40 -03:00
|
|
|
// UDT-010: Audit infrastructure health check
|
|
|
|
|
builder.Services.AddHealthChecks()
|
|
|
|
|
.AddCheck<AuditHealthCheck>("audit", tags: new[] { "audit" });
|
|
|
|
|
|
2026-04-13 21:36:08 -03:00
|
|
|
// CORS
|
|
|
|
|
var allowedOrigins = builder.Configuration
|
|
|
|
|
.GetSection("Cors:AllowedOrigins")
|
|
|
|
|
.Get<string[]>() ?? [];
|
|
|
|
|
|
|
|
|
|
builder.Services.AddCors(opts =>
|
|
|
|
|
{
|
|
|
|
|
opts.AddDefaultPolicy(policy =>
|
|
|
|
|
policy.WithOrigins(allowedOrigins)
|
|
|
|
|
.AllowAnyHeader()
|
|
|
|
|
.AllowAnyMethod());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
var app = builder.Build();
|
|
|
|
|
|
|
|
|
|
// Middleware pipeline
|
|
|
|
|
app.UseSerilogRequestLogging();
|
|
|
|
|
|
|
|
|
|
if (app.Environment.IsDevelopment() || app.Environment.IsEnvironment("Testing"))
|
|
|
|
|
{
|
|
|
|
|
app.MapOpenApi();
|
|
|
|
|
app.MapScalarApiReference(opts =>
|
|
|
|
|
{
|
|
|
|
|
opts.Title = "SIGCM2 API";
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
app.UseHttpsRedirection();
|
|
|
|
|
app.UseCors();
|
feat(api): audit context middleware + scoped impl (UDT-010 B4)
Wires the request-scoped audit context per design #D-2:
Middleware pipeline in Program.cs:
app.UseCors()
app.UseMiddleware<CorrelationIdMiddleware>() // PRE-AUTH
app.UseAuthentication()
app.UseMiddleware<AuditActorMiddleware>() // POST-AUTH
app.UseAuthorization()
app.MapControllers()
SIGCM2.Api/Middleware/CorrelationIdMiddleware.cs:
- Preserves client-sent X-Correlation-Id header when a valid GUID, otherwise
generates Guid.NewGuid(). Stores in HttpContext.Items (audit:correlationId).
- Captures Ip (Connection.RemoteIpAddress) + UserAgent header into Items.
- Echoes the correlation id back via response header (OnStarting + immediate
set — immediate set makes unit testing against DefaultHttpContext reliable).
SIGCM2.Api/Middleware/AuditActorMiddleware.cs:
- Reads JWT 'sub' claim from authenticated HttpContext.User, parses to int,
stores as audit:actorUserId. Anonymous / non-numeric sub leaves it unset.
SIGCM2.Infrastructure/Audit/AuditContext.cs (IAuditContext scoped impl):
- Reads Items entries via IHttpContextAccessor. Returns null / Guid.Empty
when no HttpContext is available (jobs, tests without middleware).
- ActorRoleId intentionally null for now — rol code → id resolution is
deferred; the logger may resolve it at persist time in a later batch.
DI registration (Infrastructure/DependencyInjection.cs):
- services.AddScoped<IAuditContext, AuditContext>()
Tests (Strict TDD):
- CorrelationIdMiddlewareTests (6): generates/preserves/handles-malformed
correlation id, sets response header, captures ip/ua, calls next.
- AuditActorMiddlewareTests (5): authenticated/anonymous/no-sub/non-numeric/
calls-next.
- AuditContextTests (7): reads from Items, null-http-context defaults,
ActorRoleId currently null.
Suite: 355/355 Application.Tests + 141/141 Api.Tests = 496/496 passing.
Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-AUD-3/9, design#D-2, tasks#B4}
2026-04-16 13:32:13 -03:00
|
|
|
// UDT-010: correlation id + ip/ua capture runs BEFORE auth so anonymous requests
|
|
|
|
|
// still get a correlation id and so logs can tie pre-auth events to the request.
|
|
|
|
|
app.UseMiddleware<CorrelationIdMiddleware>();
|
2026-04-13 21:36:08 -03:00
|
|
|
app.UseAuthentication();
|
feat(api): audit context middleware + scoped impl (UDT-010 B4)
Wires the request-scoped audit context per design #D-2:
Middleware pipeline in Program.cs:
app.UseCors()
app.UseMiddleware<CorrelationIdMiddleware>() // PRE-AUTH
app.UseAuthentication()
app.UseMiddleware<AuditActorMiddleware>() // POST-AUTH
app.UseAuthorization()
app.MapControllers()
SIGCM2.Api/Middleware/CorrelationIdMiddleware.cs:
- Preserves client-sent X-Correlation-Id header when a valid GUID, otherwise
generates Guid.NewGuid(). Stores in HttpContext.Items (audit:correlationId).
- Captures Ip (Connection.RemoteIpAddress) + UserAgent header into Items.
- Echoes the correlation id back via response header (OnStarting + immediate
set — immediate set makes unit testing against DefaultHttpContext reliable).
SIGCM2.Api/Middleware/AuditActorMiddleware.cs:
- Reads JWT 'sub' claim from authenticated HttpContext.User, parses to int,
stores as audit:actorUserId. Anonymous / non-numeric sub leaves it unset.
SIGCM2.Infrastructure/Audit/AuditContext.cs (IAuditContext scoped impl):
- Reads Items entries via IHttpContextAccessor. Returns null / Guid.Empty
when no HttpContext is available (jobs, tests without middleware).
- ActorRoleId intentionally null for now — rol code → id resolution is
deferred; the logger may resolve it at persist time in a later batch.
DI registration (Infrastructure/DependencyInjection.cs):
- services.AddScoped<IAuditContext, AuditContext>()
Tests (Strict TDD):
- CorrelationIdMiddlewareTests (6): generates/preserves/handles-malformed
correlation id, sets response header, captures ip/ua, calls next.
- AuditActorMiddlewareTests (5): authenticated/anonymous/no-sub/non-numeric/
calls-next.
- AuditContextTests (7): reads from Items, null-http-context defaults,
ActorRoleId currently null.
Suite: 355/355 Application.Tests + 141/141 Api.Tests = 496/496 passing.
Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-AUD-3/9, design#D-2, tasks#B4}
2026-04-16 13:32:13 -03:00
|
|
|
// UDT-010: actor extraction runs AFTER auth to read the JWT sub claim.
|
|
|
|
|
app.UseMiddleware<AuditActorMiddleware>();
|
2026-04-13 21:36:08 -03:00
|
|
|
app.UseAuthorization();
|
|
|
|
|
app.MapControllers();
|
|
|
|
|
|
feat(api): GET /audit/events + /health/audit (UDT-010 B10)
AuditController:
- GET /api/v1/audit/events?actorUserId&targetType&targetId&from&to&cursor&limit
- Protected by [RequirePermission("administracion:auditoria:ver")] — reuses
the existing permission (V005/V006 seed assigns it to admin).
- 400 on limit out of [1,100] or from > to.
- Cursor-based DESC pagination via AuditEventRepository.QueryAsync.
AuditHealthCheck (IHealthCheck):
- Validates SYSTEM_VERSIONING ON on Usuario/Rol/Permiso/RolPermiso.
- Validates partition boundaries exist for next 3 months (both AuditEvent and
SecurityEvent functions).
- Reports last audit event age (lenient 24h to accommodate dev/test quiet envs).
- Validates HISTORY_RETENTION_PERIOD == 10 YEARS on all 4 tables.
Key fix during impl: sys.tables.history_retention_period is stored in UNITS
(1=INFINITE, 3=DAY, 4=WEEK, 5=MONTH, 6=YEAR), NOT seconds. Assertion: period=10
AND unit=6 (10 YEARS).
- Mapped at /health/audit via app.MapHealthChecks with tag 'audit'.
Tests (Strict TDD, integration against SIGCM2_Test):
- AuditControllerTests (5): without-auth 401, without-permission 403 (cajero),
admin with filter returns events, invalid limit 400, from>to 400.
- AuditHealthCheckTests (1): returns Healthy with V010 applied.
Suite: 378/378 Application.Tests + 147/147 Api.Tests = 525/525 passing.
Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-AUD-7/8, design, tasks#B10}
2026-04-16 17:05:40 -03:00
|
|
|
// UDT-010: /health/audit returns the audit check status (public but sparse data).
|
|
|
|
|
app.MapHealthChecks("/health/audit", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions
|
|
|
|
|
{
|
|
|
|
|
Predicate = r => r.Tags.Contains("audit"),
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-13 21:36:08 -03:00
|
|
|
app.Run();
|
|
|
|
|
|
|
|
|
|
// Exposed for WebApplicationFactory in integration tests
|
|
|
|
|
public partial class Program { }
|