From ae7329034fd21e99677813a9b62270d34d20854f Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 21 May 2026 09:50:55 -0400 Subject: [PATCH] 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. --- .../NotificationOutboxActor.cs | 15 ++++++++++++--- .../Scripts/AuditingDbCommand.cs | 5 ++++- .../Scripts/ScriptRuntimeContext.cs | 10 ++++++++-- ...NotificationOutboxActorAttemptEmissionTests.cs | 4 ++-- .../Scripts/DatabaseSyncEmissionTests.cs | 3 ++- .../ExternalSystemCallAuditEmissionTests.cs | 3 ++- .../Scripts/NotifySendAuditEmissionTests.cs | 6 ++++-- 7 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/ScadaLink.NotificationOutbox/NotificationOutboxActor.cs b/src/ScadaLink.NotificationOutbox/NotificationOutboxActor.cs index eeb561c..fa61574 100644 --- a/src/ScadaLink.NotificationOutbox/NotificationOutboxActor.cs +++ b/src/ScadaLink.NotificationOutbox/NotificationOutboxActor.cs @@ -30,6 +30,13 @@ public class NotificationOutboxActor : ReceiveActor, IWithTimers private const int FallbackMaxRetries = 10; private static readonly TimeSpan FallbackRetryDelay = TimeSpan.FromMinutes(1); + /// + /// Audit Actor stamped on central-dispatch (NotifyDeliver) rows. + /// The Actor-column spec assigns central-originated audit rows a system + /// identity — there is no per-call authenticated user at dispatch time. + /// + 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, diff --git a/src/ScadaLink.SiteRuntime/Scripts/AuditingDbCommand.cs b/src/ScadaLink.SiteRuntime/Scripts/AuditingDbCommand.cs index 2936e55..223d294 100644 --- a/src/ScadaLink.SiteRuntime/Scripts/AuditingDbCommand.cs +++ b/src/ScadaLink.SiteRuntime/Scripts/AuditingDbCommand.cs @@ -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, diff --git a/src/ScadaLink.SiteRuntime/Scripts/ScriptRuntimeContext.cs b/src/ScadaLink.SiteRuntime/Scripts/ScriptRuntimeContext.cs index f7d838f..e787dd6 100644 --- a/src/ScadaLink.SiteRuntime/Scripts/ScriptRuntimeContext.cs +++ b/src/ScadaLink.SiteRuntime/Scripts/ScriptRuntimeContext.cs @@ -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, diff --git a/tests/ScadaLink.NotificationOutbox.Tests/NotificationOutboxActorAttemptEmissionTests.cs b/tests/ScadaLink.NotificationOutbox.Tests/NotificationOutboxActorAttemptEmissionTests.cs index 728ddc1..8780d59 100644 --- a/tests/ScadaLink.NotificationOutbox.Tests/NotificationOutboxActorAttemptEmissionTests.cs +++ b/tests/ScadaLink.NotificationOutbox.Tests/NotificationOutboxActorAttemptEmissionTests.cs @@ -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); }); diff --git a/tests/ScadaLink.SiteRuntime.Tests/Scripts/DatabaseSyncEmissionTests.cs b/tests/ScadaLink.SiteRuntime.Tests/Scripts/DatabaseSyncEmissionTests.cs index f2ca536..2f6f9df 100644 --- a/tests/ScadaLink.SiteRuntime.Tests/Scripts/DatabaseSyncEmissionTests.cs +++ b/tests/ScadaLink.SiteRuntime.Tests/Scripts/DatabaseSyncEmissionTests.cs @@ -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); } diff --git a/tests/ScadaLink.SiteRuntime.Tests/Scripts/ExternalSystemCallAuditEmissionTests.cs b/tests/ScadaLink.SiteRuntime.Tests/Scripts/ExternalSystemCallAuditEmissionTests.cs index 59a0e7a..3aad973 100644 --- a/tests/ScadaLink.SiteRuntime.Tests/Scripts/ExternalSystemCallAuditEmissionTests.cs +++ b/tests/ScadaLink.SiteRuntime.Tests/Scripts/ExternalSystemCallAuditEmissionTests.cs @@ -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); } diff --git a/tests/ScadaLink.SiteRuntime.Tests/Scripts/NotifySendAuditEmissionTests.cs b/tests/ScadaLink.SiteRuntime.Tests/Scripts/NotifySendAuditEmissionTests.cs index 8a85e73..7218ad9 100644 --- a/tests/ScadaLink.SiteRuntime.Tests/Scripts/NotifySendAuditEmissionTests.cs +++ b/tests/ScadaLink.SiteRuntime.Tests/Scripts/NotifySendAuditEmissionTests.cs @@ -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]