using System.Collections.Concurrent; using Campaign_Tracker.Server.Audit; namespace Campaign_Tracker.Server.Authentication; /// /// Implements IAuthenticationAuditStore by delegating durable writes to the shared /// IAuditService and maintaining an in-process queue for fast test/review queries. /// /// Both the in-memory enqueue and the IAuditService.Record() call happen synchronously. /// If IAuditService.Record() throws (audit store unavailable), the exception propagates /// to the caller — auditable actions are blocked rather than silently proceeding (AC #5). /// public sealed class InMemoryAuthenticationAuditStore : IAuthenticationAuditStore { private readonly ConcurrentQueue _events = new(); private readonly IAuditService _auditService; public InMemoryAuthenticationAuditStore(IAuditService auditService) { _auditService = auditService; } public IReadOnlyCollection Events => _events.ToArray(); public void RecordSuccess(string subject, string traceIdentifier) { var now = DateTimeOffset.UtcNow; var authEvent = new AuthenticationAuditEvent( AuthenticationAuditEventType.Success, subject, "authenticated", "authentication", traceIdentifier, now); _auditService.Record(new AuditEvent( AuditEventType.SessionLogin, subject, "authentication", "success", traceIdentifier, now)); _events.Enqueue(authEvent); } public void RecordFailure(string reason, string traceIdentifier) { var now = DateTimeOffset.UtcNow; var authEvent = new AuthenticationAuditEvent( AuthenticationAuditEventType.Failure, "anonymous", reason, "authentication", traceIdentifier, now); _auditService.Record(new AuditEvent( AuditEventType.SessionLoginFailure, "anonymous", "authentication", reason, traceIdentifier, now)); _events.Enqueue(authEvent); } public void RecordAuthorizationAllowed(string subject, string resource, string traceIdentifier) { var now = DateTimeOffset.UtcNow; var authEvent = new AuthenticationAuditEvent( AuthenticationAuditEventType.AuthorizationAllowed, subject, "authorization allowed", resource, traceIdentifier, now); _auditService.Record(new AuditEvent( AuditEventType.AuthorizationAllowed, subject, resource, "allowed", traceIdentifier, now)); _events.Enqueue(authEvent); } public void RecordAuthorizationDenied(string subject, string resource, string traceIdentifier) { var now = DateTimeOffset.UtcNow; var authEvent = new AuthenticationAuditEvent( AuthenticationAuditEventType.AuthorizationDenied, subject, "authorization denied", resource, traceIdentifier, now); _auditService.Record(new AuditEvent( AuditEventType.AuthorizationDenied, subject, resource, "denied", traceIdentifier, now)); _events.Enqueue(authEvent); } public void RecordLogout(string subject, bool succeeded, string traceIdentifier) { var now = DateTimeOffset.UtcNow; var outcome = succeeded ? "session destroyed" : "session destruction failed - keycloak unreachable"; var authEvent = new AuthenticationAuditEvent( AuthenticationAuditEventType.Logout, subject, outcome, "authentication/logout", traceIdentifier, now); _auditService.Record(new AuditEvent( AuditEventType.SessionLogout, subject, "authentication/logout", succeeded ? "success" : "failure", traceIdentifier, now)); _events.Enqueue(authEvent); } }