2 Commits

Author SHA1 Message Date
Joseph Doherty
ae7329034f fix(auditlog): populate the Actor column on outbound and central rows
Per the Audit Log Actor-column spec, Actor should carry the calling script
identity on outbound rows (ApiCall, DbWrite, NotifySend) and a system identity
on central-dispatch rows (NotifyDeliver). The original emission code hard-coded
Actor=null at all four sites, so only Inbound API rows (API key name) ever
filled it. Stamp the script identity and 'system' respectively.
2026-05-21 09:50:55 -04:00
Joseph Doherty
a3eb659b75 Merge branch 'feature/audit-log-followups': Audit Log #23 deferred follow-ups
Implements the five deferred follow-ups from the Audit Log #23 roadmap:
- Real ClusterClient-based site->central audit push (replaces NoOpSiteStreamAuditClient)
- Consolidated the duplicated AuditEvent/SiteCall DTO mappers
- Site Calls UI page + read-side backend + central->site Retry/Discard relay + Health KPI tiles
- Multi-value AuditLogQueryFilter end-to-end (repository, ManagementService, CLI, Central UI)
- Audit results grid column resize/reorder UX

Full solution build clean; full test suite green including Playwright 60/60.
2026-05-21 09:27:52 -04:00
7 changed files with 34 additions and 12 deletions

View File

@@ -30,6 +30,13 @@ public class NotificationOutboxActor : ReceiveActor, IWithTimers
private const int FallbackMaxRetries = 10;
private static readonly TimeSpan FallbackRetryDelay = TimeSpan.FromMinutes(1);
/// <summary>
/// Audit <c>Actor</c> stamped on central-dispatch (<c>NotifyDeliver</c>) rows.
/// The Actor-column spec assigns central-originated audit rows a system
/// identity — there is no per-call authenticated user at dispatch time.
/// </summary>
private const string SystemActor = "system";
private readonly IServiceProvider _serviceProvider;
private readonly NotificationOutboxOptions _options;
private readonly ICentralAuditWriter _auditWriter;
@@ -500,9 +507,11 @@ public class NotificationOutboxActor : ReceiveActor, IWithTimers
Channel = AuditChannel.Notification,
Kind = AuditKind.NotifyDeliver,
CorrelationId = correlationId,
// Central dispatch — no authenticated actor (the originating
// script's identity is captured on the upstream NotifySend row).
Actor = null,
// Central dispatch — a system identity per the Actor-column spec;
// there is no per-call authenticated user here. The originating
// script is still captured on SourceScript (and on the upstream
// NotifySend row).
Actor = SystemActor,
SourceSiteId = notification.SourceSiteId,
SourceInstanceId = notification.SourceInstanceId,
SourceScript = notification.SourceScript,

View File

@@ -430,7 +430,10 @@ internal sealed class AuditingDbCommand : DbCommand
SourceSiteId = string.IsNullOrEmpty(_siteId) ? null : _siteId,
SourceInstanceId = _instanceName,
SourceScript = _sourceScript,
Actor = null,
// Outbound channel: per the Audit Log Actor-column spec the actor is
// the calling script. Null when no single script owns the call
// (e.g. a shared script running inline).
Actor = _sourceScript,
Target = target,
Status = status,
HttpStatus = null,

View File

@@ -875,7 +875,10 @@ public class ScriptRuntimeContext
SourceSiteId = string.IsNullOrEmpty(_siteId) ? null : _siteId,
SourceInstanceId = _instanceName,
SourceScript = _sourceScript,
Actor = null,
// Outbound channel: per the Audit Log Actor-column spec the actor
// is the calling script. Null when no single script owns the call
// (e.g. a shared script running inline).
Actor = _sourceScript,
Target = $"{systemName}.{methodName}",
Status = status,
HttpStatus = httpStatus,
@@ -1355,7 +1358,10 @@ public class ScriptRuntimeContext
SourceSiteId = string.IsNullOrEmpty(_siteId) ? null : _siteId,
SourceInstanceId = _instanceName,
SourceScript = _sourceScript,
Actor = null,
// Outbound channel: per the Audit Log Actor-column spec the
// actor is the calling script. Null when no single script
// owns the call (e.g. a shared script running inline).
Actor = _sourceScript,
Target = _listName,
Status = AuditStatus.Submitted,
HttpStatus = null,

View File

@@ -155,8 +155,8 @@ public class NotificationOutboxActorAttemptEmissionTests : TestKit
Assert.Equal("site-alpha", evt.SourceSiteId);
Assert.Equal("instance-42", evt.SourceInstanceId);
Assert.Equal("AlarmScript", evt.SourceScript);
// Central dispatch: actor is null (no authenticated end-user).
Assert.Null(evt.Actor);
// Central dispatch: Actor is the system identity (no per-call user).
Assert.Equal("system", evt.Actor);
// Successful attempt: no error message.
Assert.Null(evt.ErrorMessage);
});

View File

@@ -266,7 +266,8 @@ public class DatabaseSyncEmissionTests
Assert.Equal(SiteId, evt.SourceSiteId);
Assert.Equal(InstanceName, evt.SourceInstanceId);
Assert.Equal(SourceScript, evt.SourceScript);
Assert.Null(evt.Actor);
// Outbound channel: Actor carries the calling script identity.
Assert.Equal(SourceScript, evt.Actor);
Assert.Null(evt.CorrelationId);
Assert.NotEqual(Guid.Empty, evt.EventId);
}

View File

@@ -186,7 +186,8 @@ public class ExternalSystemCallAuditEmissionTests
Assert.Equal(SiteId, evt.SourceSiteId);
Assert.Equal(InstanceName, evt.SourceInstanceId);
Assert.Equal(SourceScript, evt.SourceScript);
Assert.Null(evt.Actor);
// Outbound channel: Actor carries the calling script identity.
Assert.Equal(SourceScript, evt.Actor);
Assert.Null(evt.CorrelationId);
}

View File

@@ -127,7 +127,8 @@ public class NotifySendAuditEmissionTests : TestKit, IAsyncLifetime, IDisposable
Assert.Null(evt.HttpStatus);
Assert.Null(evt.ErrorMessage);
Assert.Null(evt.ErrorDetail);
Assert.Null(evt.Actor);
// Outbound channel: Actor carries the calling script identity.
Assert.Equal(SourceScript, evt.Actor);
}
[Fact]
@@ -199,7 +200,8 @@ public class NotifySendAuditEmissionTests : TestKit, IAsyncLifetime, IDisposable
Assert.Equal(SiteId, evt.SourceSiteId);
Assert.Equal(InstanceName, evt.SourceInstanceId);
Assert.Equal(SourceScript, evt.SourceScript);
Assert.Null(evt.Actor);
// Outbound channel: Actor carries the calling script identity.
Assert.Equal(SourceScript, evt.Actor);
}
[Fact]