Files
SIG-CM2.0/src/api/SIGCM2.Infrastructure/Audit/AuditLogger.cs

61 lines
1.9 KiB
C#
Raw Normal View History

feat(infra): AuditLogger + SecurityEventLogger impl (UDT-010 B6) Composes the audit emission layer per design #D-8: SIGCM2.Infrastructure/Audit/AuditLogger.cs (IAuditLogger): - Enriches from IAuditContext (ActorUserId/ActorRoleId/Ip/UserAgent/CorrelationId). - Sanitizes metadata via JsonSanitizer + AuditOptions.SanitizedKeys. - Persists via IAuditEventRepository.InsertAsync. - Fail-closed: throws AuditContextMissingException when ActorUserId is null. - Translates Guid.Empty correlation id to null (DB column is nullable; Empty indicates 'no middleware ran'). - Uses System.DateTime.UtcNow for occurredAt. SIGCM2.Infrastructure/Audit/SecurityEventLogger.cs (ISecurityEventLogger): - NOT fail-closed: null ActorUserId is valid (login failures, anonymous permission.denied events). - Ip/UserAgent pulled from IAuditContext; metadata sanitized the same way. - Persists via ISecurityEventRepository. DI: AddScoped for both loggers in AddInfrastructure. Tests (Strict TDD, mocks for IAuditContext/IAuditEventRepository/ ISecurityEventRepository): - AuditLoggerTests (6): happy path with full context, fail-closed null actor, metadata sanitization, null metadata pass-through, repo-throws-bubbles-up (critical for TransactionScope rollback), custom SanitizedKeys from options. - SecurityEventLoggerTests (4): login.success with context, login.failure with null actor + attemptedUsername, metadata sanitization, permission.denied with both actor and attemptedUsername null. Two initial failures were fixed by replacing 'null' literal arguments in NSubstitute Received(...) assertions with Arg.Is<T?>(x => x == null) — NSubstitute does not always match null literals when mixed with Arg.Any<T>(). Suite: 378/378 Application.Tests + 141/141 Api.Tests = 519/519 passing. Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-AUD-4 #REQ-SEC-2/3, design#D-8, tasks#B6}
2026-04-16 13:41:10 -03:00
using Microsoft.Extensions.Options;
using SIGCM2.Application.Audit;
using SIGCM2.Domain.Exceptions;
namespace SIGCM2.Infrastructure.Audit;
/// UDT-010 — IAuditLogger implementation. Enriches from IAuditContext, sanitizes metadata,
/// persists via IAuditEventRepository. Fail-closed: throws AuditContextMissingException
/// when ActorUserId is null (system-emitted events should use a different path).
public sealed class AuditLogger : IAuditLogger
{
private readonly IAuditContext _context;
private readonly IAuditEventRepository _repo;
private readonly IOptions<AuditOptions> _options;
private readonly TimeProvider _timeProvider;
feat(infra): AuditLogger + SecurityEventLogger impl (UDT-010 B6) Composes the audit emission layer per design #D-8: SIGCM2.Infrastructure/Audit/AuditLogger.cs (IAuditLogger): - Enriches from IAuditContext (ActorUserId/ActorRoleId/Ip/UserAgent/CorrelationId). - Sanitizes metadata via JsonSanitizer + AuditOptions.SanitizedKeys. - Persists via IAuditEventRepository.InsertAsync. - Fail-closed: throws AuditContextMissingException when ActorUserId is null. - Translates Guid.Empty correlation id to null (DB column is nullable; Empty indicates 'no middleware ran'). - Uses System.DateTime.UtcNow for occurredAt. SIGCM2.Infrastructure/Audit/SecurityEventLogger.cs (ISecurityEventLogger): - NOT fail-closed: null ActorUserId is valid (login failures, anonymous permission.denied events). - Ip/UserAgent pulled from IAuditContext; metadata sanitized the same way. - Persists via ISecurityEventRepository. DI: AddScoped for both loggers in AddInfrastructure. Tests (Strict TDD, mocks for IAuditContext/IAuditEventRepository/ ISecurityEventRepository): - AuditLoggerTests (6): happy path with full context, fail-closed null actor, metadata sanitization, null metadata pass-through, repo-throws-bubbles-up (critical for TransactionScope rollback), custom SanitizedKeys from options. - SecurityEventLoggerTests (4): login.success with context, login.failure with null actor + attemptedUsername, metadata sanitization, permission.denied with both actor and attemptedUsername null. Two initial failures were fixed by replacing 'null' literal arguments in NSubstitute Received(...) assertions with Arg.Is<T?>(x => x == null) — NSubstitute does not always match null literals when mixed with Arg.Any<T>(). Suite: 378/378 Application.Tests + 141/141 Api.Tests = 519/519 passing. Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-AUD-4 #REQ-SEC-2/3, design#D-8, tasks#B6}
2026-04-16 13:41:10 -03:00
public AuditLogger(
IAuditContext context,
IAuditEventRepository repo,
IOptions<AuditOptions> options,
TimeProvider timeProvider)
feat(infra): AuditLogger + SecurityEventLogger impl (UDT-010 B6) Composes the audit emission layer per design #D-8: SIGCM2.Infrastructure/Audit/AuditLogger.cs (IAuditLogger): - Enriches from IAuditContext (ActorUserId/ActorRoleId/Ip/UserAgent/CorrelationId). - Sanitizes metadata via JsonSanitizer + AuditOptions.SanitizedKeys. - Persists via IAuditEventRepository.InsertAsync. - Fail-closed: throws AuditContextMissingException when ActorUserId is null. - Translates Guid.Empty correlation id to null (DB column is nullable; Empty indicates 'no middleware ran'). - Uses System.DateTime.UtcNow for occurredAt. SIGCM2.Infrastructure/Audit/SecurityEventLogger.cs (ISecurityEventLogger): - NOT fail-closed: null ActorUserId is valid (login failures, anonymous permission.denied events). - Ip/UserAgent pulled from IAuditContext; metadata sanitized the same way. - Persists via ISecurityEventRepository. DI: AddScoped for both loggers in AddInfrastructure. Tests (Strict TDD, mocks for IAuditContext/IAuditEventRepository/ ISecurityEventRepository): - AuditLoggerTests (6): happy path with full context, fail-closed null actor, metadata sanitization, null metadata pass-through, repo-throws-bubbles-up (critical for TransactionScope rollback), custom SanitizedKeys from options. - SecurityEventLoggerTests (4): login.success with context, login.failure with null actor + attemptedUsername, metadata sanitization, permission.denied with both actor and attemptedUsername null. Two initial failures were fixed by replacing 'null' literal arguments in NSubstitute Received(...) assertions with Arg.Is<T?>(x => x == null) — NSubstitute does not always match null literals when mixed with Arg.Any<T>(). Suite: 378/378 Application.Tests + 141/141 Api.Tests = 519/519 passing. Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-AUD-4 #REQ-SEC-2/3, design#D-8, tasks#B6}
2026-04-16 13:41:10 -03:00
{
_context = context;
_repo = repo;
_options = options;
_timeProvider = timeProvider;
feat(infra): AuditLogger + SecurityEventLogger impl (UDT-010 B6) Composes the audit emission layer per design #D-8: SIGCM2.Infrastructure/Audit/AuditLogger.cs (IAuditLogger): - Enriches from IAuditContext (ActorUserId/ActorRoleId/Ip/UserAgent/CorrelationId). - Sanitizes metadata via JsonSanitizer + AuditOptions.SanitizedKeys. - Persists via IAuditEventRepository.InsertAsync. - Fail-closed: throws AuditContextMissingException when ActorUserId is null. - Translates Guid.Empty correlation id to null (DB column is nullable; Empty indicates 'no middleware ran'). - Uses System.DateTime.UtcNow for occurredAt. SIGCM2.Infrastructure/Audit/SecurityEventLogger.cs (ISecurityEventLogger): - NOT fail-closed: null ActorUserId is valid (login failures, anonymous permission.denied events). - Ip/UserAgent pulled from IAuditContext; metadata sanitized the same way. - Persists via ISecurityEventRepository. DI: AddScoped for both loggers in AddInfrastructure. Tests (Strict TDD, mocks for IAuditContext/IAuditEventRepository/ ISecurityEventRepository): - AuditLoggerTests (6): happy path with full context, fail-closed null actor, metadata sanitization, null metadata pass-through, repo-throws-bubbles-up (critical for TransactionScope rollback), custom SanitizedKeys from options. - SecurityEventLoggerTests (4): login.success with context, login.failure with null actor + attemptedUsername, metadata sanitization, permission.denied with both actor and attemptedUsername null. Two initial failures were fixed by replacing 'null' literal arguments in NSubstitute Received(...) assertions with Arg.Is<T?>(x => x == null) — NSubstitute does not always match null literals when mixed with Arg.Any<T>(). Suite: 378/378 Application.Tests + 141/141 Api.Tests = 519/519 passing. Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-AUD-4 #REQ-SEC-2/3, design#D-8, tasks#B6}
2026-04-16 13:41:10 -03:00
}
public async Task LogAsync(
string action,
string targetType,
string targetId,
object? metadata = null,
CancellationToken ct = default)
{
if (_context.ActorUserId is null)
throw new AuditContextMissingException();
var sanitized = metadata is null
? null
: JsonSanitizer.Sanitize(metadata, _options.Value.SanitizedKeys);
var correlationId = _context.CorrelationId == Guid.Empty
? (Guid?)null
: _context.CorrelationId;
await _repo.InsertAsync(
occurredAt: _timeProvider.GetUtcNow().UtcDateTime,
feat(infra): AuditLogger + SecurityEventLogger impl (UDT-010 B6) Composes the audit emission layer per design #D-8: SIGCM2.Infrastructure/Audit/AuditLogger.cs (IAuditLogger): - Enriches from IAuditContext (ActorUserId/ActorRoleId/Ip/UserAgent/CorrelationId). - Sanitizes metadata via JsonSanitizer + AuditOptions.SanitizedKeys. - Persists via IAuditEventRepository.InsertAsync. - Fail-closed: throws AuditContextMissingException when ActorUserId is null. - Translates Guid.Empty correlation id to null (DB column is nullable; Empty indicates 'no middleware ran'). - Uses System.DateTime.UtcNow for occurredAt. SIGCM2.Infrastructure/Audit/SecurityEventLogger.cs (ISecurityEventLogger): - NOT fail-closed: null ActorUserId is valid (login failures, anonymous permission.denied events). - Ip/UserAgent pulled from IAuditContext; metadata sanitized the same way. - Persists via ISecurityEventRepository. DI: AddScoped for both loggers in AddInfrastructure. Tests (Strict TDD, mocks for IAuditContext/IAuditEventRepository/ ISecurityEventRepository): - AuditLoggerTests (6): happy path with full context, fail-closed null actor, metadata sanitization, null metadata pass-through, repo-throws-bubbles-up (critical for TransactionScope rollback), custom SanitizedKeys from options. - SecurityEventLoggerTests (4): login.success with context, login.failure with null actor + attemptedUsername, metadata sanitization, permission.denied with both actor and attemptedUsername null. Two initial failures were fixed by replacing 'null' literal arguments in NSubstitute Received(...) assertions with Arg.Is<T?>(x => x == null) — NSubstitute does not always match null literals when mixed with Arg.Any<T>(). Suite: 378/378 Application.Tests + 141/141 Api.Tests = 519/519 passing. Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-AUD-4 #REQ-SEC-2/3, design#D-8, tasks#B6}
2026-04-16 13:41:10 -03:00
actorUserId: _context.ActorUserId,
actorRoleId: _context.ActorRoleId,
action: action,
targetType: targetType,
targetId: targetId,
correlationId: correlationId,
ipAddress: _context.Ip,
userAgent: _context.UserAgent,
metadata: sanitized,
ct: ct);
}
}