fix(audit): route SecuredWrite audit via ICentralAuditWriter for SourceNode stamping (#206)

This commit is contained in:
Joseph Doherty
2026-06-19 02:37:48 -04:00
parent fb18253f32
commit 69e0d546b5
3 changed files with 64 additions and 11 deletions
@@ -969,15 +969,25 @@ public class ManagementActor : ReceiveActor
/// <summary>
/// Best-effort emission of ONE secured-write AuditLog row via the central direct-write
/// path (<see cref="IAuditLogRepository.InsertIfNotExistsAsync"/>) — mirrors the
/// Notification Outbox / Inbound API central-origin pattern. The row is built through
/// the canonical <see cref="ScadaBridgeAuditEventFactory"/> so Action/Category/Outcome
/// map identically to every other emit site.
/// path (<see cref="ICentralAuditWriter.WriteAsync"/>) — mirrors the Notification
/// Outbox dispatch / Inbound API central-origin pattern. The row is built through the
/// canonical <see cref="ScadaBridgeAuditEventFactory"/> so Action/Category/Outcome map
/// identically to every other emit site.
/// </summary>
/// <remarks>
/// #206: routes through <see cref="ICentralAuditWriter"/> (rather than calling
/// <see cref="IAuditLogRepository.InsertIfNotExistsAsync"/> directly) so the writing
/// central node's <c>SourceNode</c> (<c>central-a</c>/<c>central-b</c>) is stamped via
/// the writer's <see cref="INodeIdentityProvider"/> — satisfying the design's
/// node-of-origin invariant. The rows are otherwise unchanged (same channel/kind,
/// same <c>CorrelationId</c> = the row id, same payload).
/// <para>
/// Standing audit invariant: an audit-write failure NEVER aborts the secured-write
/// action. Every exception (repository resolution OR the insert) is caught, logged at
/// warning, and swallowed — the caller's own success/failure path is authoritative.
/// action. <see cref="ICentralAuditWriter"/> is best-effort and swallows its own
/// internal failures; the surrounding try/catch is defensive belt-and-braces (it also
/// guards writer resolution) — either way the caller's own success/failure path is
/// authoritative.
/// </para>
/// </remarks>
private static async Task EmitSecuredWriteAuditAsync(
IServiceProvider sp,
@@ -1006,9 +1016,11 @@ public class ManagementActor : ReceiveActor
verifierUser = row.VerifierUser
}));
using var scope = sp.CreateScope();
var auditRepo = scope.ServiceProvider.GetRequiredService<IAuditLogRepository>();
await auditRepo.InsertIfNotExistsAsync(evt);
// #206: the central direct-write writer is a singleton that opens its own
// per-call scope for the (scoped) IAuditLogRepository and stamps SourceNode
// from the local INodeIdentityProvider — no scope needed here.
var auditWriter = sp.GetRequiredService<ICentralAuditWriter>();
await auditWriter.WriteAsync(evt);
}
catch (Exception ex)
{