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.
This commit is contained in:
Joseph Doherty
2026-05-21 09:50:55 -04:00
parent a3eb659b75
commit ae7329034f
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 const int FallbackMaxRetries = 10;
private static readonly TimeSpan FallbackRetryDelay = TimeSpan.FromMinutes(1); 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 IServiceProvider _serviceProvider;
private readonly NotificationOutboxOptions _options; private readonly NotificationOutboxOptions _options;
private readonly ICentralAuditWriter _auditWriter; private readonly ICentralAuditWriter _auditWriter;
@@ -500,9 +507,11 @@ public class NotificationOutboxActor : ReceiveActor, IWithTimers
Channel = AuditChannel.Notification, Channel = AuditChannel.Notification,
Kind = AuditKind.NotifyDeliver, Kind = AuditKind.NotifyDeliver,
CorrelationId = correlationId, CorrelationId = correlationId,
// Central dispatch — no authenticated actor (the originating // Central dispatch — a system identity per the Actor-column spec;
// script's identity is captured on the upstream NotifySend row). // there is no per-call authenticated user here. The originating
Actor = null, // script is still captured on SourceScript (and on the upstream
// NotifySend row).
Actor = SystemActor,
SourceSiteId = notification.SourceSiteId, SourceSiteId = notification.SourceSiteId,
SourceInstanceId = notification.SourceInstanceId, SourceInstanceId = notification.SourceInstanceId,
SourceScript = notification.SourceScript, SourceScript = notification.SourceScript,

View File

@@ -430,7 +430,10 @@ internal sealed class AuditingDbCommand : DbCommand
SourceSiteId = string.IsNullOrEmpty(_siteId) ? null : _siteId, SourceSiteId = string.IsNullOrEmpty(_siteId) ? null : _siteId,
SourceInstanceId = _instanceName, SourceInstanceId = _instanceName,
SourceScript = _sourceScript, 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, Target = target,
Status = status, Status = status,
HttpStatus = null, HttpStatus = null,

View File

@@ -875,7 +875,10 @@ public class ScriptRuntimeContext
SourceSiteId = string.IsNullOrEmpty(_siteId) ? null : _siteId, SourceSiteId = string.IsNullOrEmpty(_siteId) ? null : _siteId,
SourceInstanceId = _instanceName, SourceInstanceId = _instanceName,
SourceScript = _sourceScript, 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}", Target = $"{systemName}.{methodName}",
Status = status, Status = status,
HttpStatus = httpStatus, HttpStatus = httpStatus,
@@ -1355,7 +1358,10 @@ public class ScriptRuntimeContext
SourceSiteId = string.IsNullOrEmpty(_siteId) ? null : _siteId, SourceSiteId = string.IsNullOrEmpty(_siteId) ? null : _siteId,
SourceInstanceId = _instanceName, SourceInstanceId = _instanceName,
SourceScript = _sourceScript, 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, Target = _listName,
Status = AuditStatus.Submitted, Status = AuditStatus.Submitted,
HttpStatus = null, HttpStatus = null,

View File

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

View File

@@ -266,7 +266,8 @@ public class DatabaseSyncEmissionTests
Assert.Equal(SiteId, evt.SourceSiteId); Assert.Equal(SiteId, evt.SourceSiteId);
Assert.Equal(InstanceName, evt.SourceInstanceId); Assert.Equal(InstanceName, evt.SourceInstanceId);
Assert.Equal(SourceScript, evt.SourceScript); 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.Null(evt.CorrelationId);
Assert.NotEqual(Guid.Empty, evt.EventId); Assert.NotEqual(Guid.Empty, evt.EventId);
} }

View File

@@ -186,7 +186,8 @@ public class ExternalSystemCallAuditEmissionTests
Assert.Equal(SiteId, evt.SourceSiteId); Assert.Equal(SiteId, evt.SourceSiteId);
Assert.Equal(InstanceName, evt.SourceInstanceId); Assert.Equal(InstanceName, evt.SourceInstanceId);
Assert.Equal(SourceScript, evt.SourceScript); 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.Null(evt.CorrelationId);
} }

View File

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