feat(audit): AuditEvent record + AuditOutcome + writer/redactor seams
Includes equality-as-normalized-instant remarks on OccurredAtUtc and a same-instant/different-offset equality regression test (code-review follow-up).
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
namespace ZB.MOM.WW.Audit;
|
||||
|
||||
/// <summary>
|
||||
/// Canonical, transport-agnostic audit record — who did what, when, with what outcome.
|
||||
/// Required core + optional common fields + a <see cref="DetailsJson"/> extension bag. Each
|
||||
/// sister app maps its own record onto this; domain vocabularies (channels/kinds/event-types)
|
||||
/// map into <see cref="Action"/>/<see cref="Category"/>/<see cref="DetailsJson"/> and are not
|
||||
/// modelled here. See scadaproj/components/audit/spec/EVENT-MODEL.md.
|
||||
/// </summary>
|
||||
public sealed record AuditEvent
|
||||
{
|
||||
/// <summary>Idempotency key uniquely identifying this audit event.</summary>
|
||||
public required Guid EventId { get; init; }
|
||||
|
||||
/// <summary>When the audited action occurred. Normalized to UTC on assignment.</summary>
|
||||
/// <remarks>Participates in record value-equality as a normalized instant: two events whose
|
||||
/// <c>OccurredAtUtc</c> denote the same instant at different offsets (e.g. <c>12:00+05:00</c> and
|
||||
/// <c>07:00Z</c>) compare equal and share a hash code. Relevant to consumers that dedup/key on
|
||||
/// <see cref="AuditEvent"/> value-equality.</remarks>
|
||||
public required DateTimeOffset OccurredAtUtc
|
||||
{
|
||||
get => _occurredAtUtc;
|
||||
init => _occurredAtUtc = value.ToUniversalTime();
|
||||
}
|
||||
private readonly DateTimeOffset _occurredAtUtc;
|
||||
|
||||
/// <summary>Who performed the action (identity string; the ZB.MOM.WW.Auth principal at adoption).</summary>
|
||||
public required string Actor { get; init; }
|
||||
|
||||
/// <summary>What was done — a verb/event-type string.</summary>
|
||||
public required string Action { get; init; }
|
||||
|
||||
/// <summary>Normalized outcome.</summary>
|
||||
public required AuditOutcome Outcome { get; init; }
|
||||
|
||||
/// <summary>Optional subsystem/grouping for the action.</summary>
|
||||
public string? Category { get; init; }
|
||||
|
||||
/// <summary>Optional target of the action (resource/method/connection).</summary>
|
||||
public string? Target { get; init; }
|
||||
|
||||
/// <summary>Optional node that emitted the event.</summary>
|
||||
public string? SourceNode { get; init; }
|
||||
|
||||
/// <summary>Optional correlation id joining this row to its originating request/workflow.</summary>
|
||||
public Guid? CorrelationId { get; init; }
|
||||
|
||||
/// <summary>Optional JSON extension carrying project-specific fields.</summary>
|
||||
public string? DetailsJson { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace ZB.MOM.WW.Audit;
|
||||
|
||||
/// <summary>Normalized outcome of an audited action.</summary>
|
||||
public enum AuditOutcome
|
||||
{
|
||||
/// <summary>The action completed successfully.</summary>
|
||||
Success,
|
||||
/// <summary>The action failed due to an error.</summary>
|
||||
Failure,
|
||||
/// <summary>The action was rejected by authentication/authorization.</summary>
|
||||
Denied,
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace ZB.MOM.WW.Audit;
|
||||
|
||||
/// <summary>
|
||||
/// Filters an <see cref="AuditEvent"/> between construction and persistence — truncates oversized
|
||||
/// fields and scrubs sensitive content. Pure function: returns a filtered COPY and MUST NOT throw
|
||||
/// (over-redact on internal failure). Shaped to mirror Telemetry's <c>ILogRedactor</c> so a future
|
||||
/// ZB.MOM.WW.Hosting aggregator can wire both consistently; intentionally has no dependency on it.
|
||||
/// </summary>
|
||||
public interface IAuditRedactor
|
||||
{
|
||||
/// <summary>Apply the configured truncation/redaction policy and return a filtered copy.</summary>
|
||||
AuditEvent Apply(AuditEvent rawEvent);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace ZB.MOM.WW.Audit;
|
||||
|
||||
/// <summary>
|
||||
/// Best-effort sink for <see cref="AuditEvent"/>s. Implementations MUST swallow/log internal
|
||||
/// failures rather than propagating them — a failed audit write must never abort the
|
||||
/// user-facing action that produced it.
|
||||
/// </summary>
|
||||
public interface IAuditWriter
|
||||
{
|
||||
/// <summary>Persist an audit event. Best-effort; must not throw to the caller.</summary>
|
||||
Task WriteAsync(AuditEvent evt, CancellationToken ct = default);
|
||||
}
|
||||
Reference in New Issue
Block a user