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);
}
}