feat(adminui): IAuditWriter adapter + cert-action audit-event factory

This commit is contained in:
Joseph Doherty
2026-06-19 00:31:37 -04:00
parent 21c881161a
commit 084d73ea2b
6 changed files with 254 additions and 0 deletions
@@ -0,0 +1,39 @@
using Akka.Actor;
using Akka.Hosting;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.OtOpcUa.ControlPlane;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Audit;
/// <summary>
/// AdminUI-side <see cref="IAuditWriter" /> adapter. Forwards canonical
/// <see cref="AuditEvent" />s to the ControlPlane <see cref="AuditWriterActorKey" /> cluster
/// singleton (resolved from <see cref="ActorRegistry" /> at construction, mirroring
/// <c>AdminOperationsClient</c>), which batches and persists them to <c>ConfigAuditLog</c>.
/// Emission is fire-and-forget via <c>Tell</c>: best-effort and never blocks or throws to the
/// caller, matching the audit-writer contract.
/// </summary>
public sealed class ActorAuditWriter : IAuditWriter
{
private readonly IActorRef _proxy;
/// <summary>Initializes a new instance of the <see cref="ActorAuditWriter" /> class.</summary>
/// <param name="registry">The actor registry used to resolve the AuditWriter singleton proxy.</param>
public ActorAuditWriter(ActorRegistry registry)
{
_proxy = registry.Get<AuditWriterActorKey>();
}
/// <summary>
/// Forwards the event to the AuditWriter singleton via <c>Tell</c> and returns immediately.
/// The actual persistence happens asynchronously on the actor's next flush.
/// </summary>
/// <param name="evt">The canonical audit event to persist.</param>
/// <param name="ct">Unused — enqueue is synchronous and non-blocking.</param>
/// <returns>A completed task.</returns>
public Task WriteAsync(AuditEvent evt, CancellationToken ct = default)
{
_proxy.Tell(evt);
return Task.CompletedTask;
}
}
@@ -0,0 +1,56 @@
using System.Text.Json;
using ZB.MOM.WW.Audit;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Audit;
/// <summary>
/// Pure factory that maps a certificate-store action (Trust / Untrust / Delete of a peer
/// certificate) into a canonical <see cref="AuditEvent" /> for persistence via the shared
/// <see cref="IAuditWriter" /> seam. No side effects: callers build the event then hand it to
/// the writer.
/// </summary>
public static class CertAuditEvents
{
/// <summary>The audit <see cref="AuditEvent.Category" /> stamped onto every certificate event.</summary>
public const string Category = "Certificate";
/// <summary>
/// Builds a canonical <see cref="AuditEvent" /> describing a certificate-store action.
/// </summary>
/// <param name="action">The action verb — e.g. <c>Trust</c>, <c>Untrust</c>, or <c>Delete</c>.
/// Stamped onto <see cref="AuditEvent.Action" />.</param>
/// <param name="store">The certificate store the action targeted (carried in the details payload).</param>
/// <param name="thumbprint">The subject certificate's thumbprint; stamped onto
/// <see cref="AuditEvent.SourceNode" /> and echoed in the details payload.</param>
/// <param name="actor">The identity that performed the action; stamped onto
/// <see cref="AuditEvent.Actor" />.</param>
/// <param name="success"><see langword="true" /> if the action succeeded (Outcome
/// <see cref="AuditOutcome.Success" />); otherwise <see langword="false" /> (Outcome
/// <see cref="AuditOutcome.Failure" />).</param>
/// <param name="error">On failure, the error text carried in the details payload; ignored on
/// success (the details payload serializes <see langword="null" /> for the error field).</param>
/// <returns>A fully populated <see cref="AuditEvent" /> with a fresh <see cref="AuditEvent.EventId" />
/// and <see cref="AuditEvent.OccurredAtUtc" /> set to now (UTC).</returns>
public static AuditEvent Build(
string action, string store, string thumbprint, string actor, bool success, string? error)
{
var detailsJson = JsonSerializer.Serialize(new
{
store,
thumbprint,
error = success ? null : error,
});
return new AuditEvent
{
EventId = Guid.NewGuid(),
OccurredAtUtc = DateTimeOffset.UtcNow,
Actor = actor,
Action = action,
Category = Category,
SourceNode = thumbprint,
Outcome = success ? AuditOutcome.Success : AuditOutcome.Failure,
DetailsJson = detailsJson,
};
}
}
@@ -54,6 +54,9 @@ public static class EndpointRouteBuilderExtensions
// Certificate-store actions (trust/untrust/delete) for the /certificates page.
services.AddSingleton<Certificates.CertificateStoreManager>();
// Structured audit-event sink: forwards AuditEvents to the ControlPlane AuditWriter singleton.
services.AddSingleton<ZB.MOM.WW.Audit.IAuditWriter, Audit.ActorAuditWriter>();
return services;
}
}