feat(application): audit abstractions (UDT-010 B2)

Introduces the contract layer for audit logging per design #D-8:

SIGCM2.Application/Audit/:
- IAuditContext — request-scoped accessor for ActorUserId/ActorRoleId/
  Ip/UserAgent/CorrelationId. Populated by CorrelationIdMiddleware +
  AuditActorMiddleware (B4).
- IAuditLogger.LogAsync(action, targetType, targetId, metadata?, ct) —
  domain-level audit emitter. Enlists in ambient TransactionScope
  (fail-closed per #REQ-AUD-4).
- ISecurityEventLogger.LogAsync(action, result, actorUserId?, attemptedUsername?,
  sessionId?, failureReason?, metadata?, ct) — security-events emitter
  separate from IAuditLogger (different retention, no transaction scope,
  captures login/logout/refresh/permission.denied).
- AuditOptions — bindable POCO with SanitizedKeys[] defaults (used by
  JsonSanitizer in B3).
- AuditEventDto — read projection for GET /api/v1/audit/events (B10).
- AuditEventFilter — query filter record with default Limit=50.

SIGCM2.Domain/Exceptions/:
- AuditContextMissingException : DomainException — fail-closed sentinel
  thrown when IAuditLogger is called without ActorUserId in a user-scoped
  command (#REQ-AUD-4).

Tests (Strict TDD — shape contract, RED → GREEN):
- tests/SIGCM2.Application.Tests/Audit/AuditAbstractionsTests.cs: 11 tests
  covering nullability, signatures, default options, record equality.

Suite: 336/336 Application.Tests (prev 325 + 11 new). 130/130 Api.Tests.

Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-AUD-3/4/5, design#D-8, tasks#B2}
This commit is contained in:
2026-04-16 13:23:11 -03:00
parent c95bc7fe01
commit 68f96b90c7
8 changed files with 290 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
namespace SIGCM2.Application.Audit;
/// Read projection of dbo.AuditEvent for GET /api/v1/audit/events.
/// ActorUsername is resolved by join against dbo.Usuario at query time (denormalized in the DTO).
public sealed record AuditEventDto(
long Id,
DateTime OccurredAt,
int? ActorUserId,
string? ActorUsername,
string Action,
string TargetType,
string TargetId,
Guid? CorrelationId,
string? IpAddress,
string? Metadata);

View File

@@ -0,0 +1,13 @@
namespace SIGCM2.Application.Audit;
/// Filter criteria for GET /api/v1/audit/events.
/// Cursor is opaque to the caller (base64-encoded {OccurredAt, Id} tuple for stable DESC pagination).
/// Limit is clamped between 1 and 100 at the validation layer; default 50 is the sane middle.
public sealed record AuditEventFilter(
int? ActorUserId,
string? TargetType,
string? TargetId,
DateTime? From,
DateTime? To,
string? Cursor,
int Limit = 50);

View File

@@ -0,0 +1,22 @@
namespace SIGCM2.Application.Audit;
/// Bound from appsettings section "Audit". Extensible via configuration.
public sealed class AuditOptions
{
public const string SectionName = "Audit";
public string[] SanitizedKeys { get; set; } =
[
"password",
"passwordHash",
"token",
"refreshToken",
"accessToken",
"cvv",
"card",
"cardNumber",
"secret",
"apiKey",
"privateKey",
];
}

View File

@@ -0,0 +1,14 @@
namespace SIGCM2.Application.Audit;
/// Request-scoped context populated by the audit middleware pipeline:
/// CorrelationIdMiddleware (pre-auth) sets CorrelationId/Ip/UserAgent;
/// AuditActorMiddleware (post-auth) fills ActorUserId/ActorRoleId from JWT claims.
/// Consumed by IAuditLogger to enrich every AuditEvent emitted within the request.
public interface IAuditContext
{
int? ActorUserId { get; }
int? ActorRoleId { get; }
string? Ip { get; }
string? UserAgent { get; }
Guid CorrelationId { get; }
}

View File

@@ -0,0 +1,16 @@
namespace SIGCM2.Application.Audit;
/// Emits dbo.AuditEvent rows for domain-level actions (create/update/delete of business entities).
/// LogAsync enlists in the ambient TransactionScope — if the INSERT fails, the caller's command rolls back.
/// Metadata is sanitized (see AuditOptions.SanitizedKeys) before persisting.
/// If IAuditContext.ActorUserId is null while called from a user-scoped command, an
/// AuditContextMissingException is thrown — fail-closed by design.
public interface IAuditLogger
{
Task LogAsync(
string action,
string targetType,
string targetId,
object? metadata = null,
CancellationToken ct = default);
}

View File

@@ -0,0 +1,20 @@
namespace SIGCM2.Application.Audit;
/// Emits dbo.SecurityEvent rows for auth/authorization events (login, logout, refresh, permission.denied).
/// Separate from IAuditLogger because:
/// - Volume is ~100× higher than AuditEvent (retention 5y vs 10y).
/// - Invoked from infrastructure layers (auth handlers, middlewares) where no ambient
/// business transaction exists — not enlisted in TransactionScope.
/// - Schema includes Result/FailureReason/AttemptedUsername/SessionId specific to security events.
public interface ISecurityEventLogger
{
Task LogAsync(
string action,
string result,
int? actorUserId = null,
string? attemptedUsername = null,
Guid? sessionId = null,
string? failureReason = null,
object? metadata = null,
CancellationToken ct = default);
}