b7f5e887ee
Persist the canonical AuditOutcome and make structured audit rows visible. - ConfigAuditLog gains a nullable Outcome column, stored as the AuditOutcome enum member name (nvarchar(16), mirroring how AdminRole is persisted). The AuditWriterActor flush now writes Outcome = evt.Outcome.ToString(). Nullable so legacy rows and the bespoke stored-procedure path (no derived outcome) write NULL. - Migration 20260602135350_AddConfigAuditLogOutcome: additive nullable column, no backfill. Up adds the column, Down drops it. Chains after 20260602112419_CanonicalizeAdminRoles; `dotnet ef migrations has-pending-model-changes` is clean. - ClusterAudit visibility fix: the page filtered solely on ClusterId, but the structured AuditWriterActor path stamps NodeId (ClusterId null), so those rows were invisible. Extracted ClusterAuditQuery.ForClusterAsync (shared by the page and tests) which ORs in rows whose NodeId belongs to a node in the cluster — membership resolved from ClusterNode (NodeId -> ClusterId). SP-path ClusterId-stamped rows still match. Tests: ControlPlane 45/45 (adds Outcome persistence + Denied-outcome asserts); new Configuration ClusterAuditQueryTests 3/3 (both-paths visible, other-cluster excluded, page-size cap); AdminUI 121/121. Configuration Unit suite is green on a clean run (a pre-existing timing flake in ResilientConfigReaderTests, untouched here, occasionally fails under parallel load and passes in isolation).
51 lines
2.6 KiB
C#
51 lines
2.6 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
|
|
|
/// <summary>
|
|
/// Append-only audit log for every config write + authorization-check event. Grants revoked for
|
|
/// UPDATE / DELETE on all principals (enforced by the authorization migration in B.3).
|
|
/// </summary>
|
|
public sealed class ConfigAuditLog
|
|
{
|
|
/// <summary>Gets or sets the unique audit log identifier.</summary>
|
|
public long AuditId { get; set; }
|
|
|
|
/// <summary>Gets or sets the timestamp of the audit event.</summary>
|
|
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
|
|
|
/// <summary>Gets or sets the principal (user or service) that initiated the event.</summary>
|
|
public required string Principal { get; set; }
|
|
|
|
/// <summary>DraftCreated | DraftEdited | Published | RolledBack | NodeApplied | CredentialAdded | CredentialDisabled | ClusterCreated | NodeAdded | ExternalIdReleased | CrossClusterNamespaceAttempt | OpcUaAccessDenied | …</summary>
|
|
public required string EventType { get; set; }
|
|
|
|
/// <summary>Gets or sets the cluster identifier associated with the event, if applicable.</summary>
|
|
public string? ClusterId { get; set; }
|
|
|
|
/// <summary>Gets or sets the node identifier associated with the event, if applicable.</summary>
|
|
public string? NodeId { get; set; }
|
|
|
|
/// <summary>Gets or sets the generation identifier associated with the event, if applicable.</summary>
|
|
public long? GenerationId { get; set; }
|
|
|
|
/// <summary>Gets or sets additional event details in JSON format.</summary>
|
|
public string? DetailsJson { get; set; }
|
|
|
|
/// <summary>
|
|
/// Stable per-event identifier from <c>AuditEvent.EventId</c>. Filtered unique index on
|
|
/// this column gives cross-restart idempotency for the batched AuditWriterActor: a flush
|
|
/// that retries after a process crash can re-send the same EventId without producing a
|
|
/// duplicate row. Nullable so pre-v2 rows backfill cleanly.
|
|
/// </summary>
|
|
public Guid? EventId { get; set; }
|
|
|
|
/// <summary>Correlation ID from <c>AuditEvent.CorrelationId</c> so an audit row joins to its
|
|
/// originating request/workflow. Nullable for the same backfill reason as <see cref="EventId"/>.</summary>
|
|
public Guid? CorrelationId { get; set; }
|
|
|
|
/// <summary>Normalized outcome from <c>AuditEvent.Outcome</c> (the canonical
|
|
/// <c>ZB.MOM.WW.Audit.AuditOutcome</c>: <c>Success</c> | <c>Failure</c> | <c>Denied</c>),
|
|
/// stored as its enum member name. Nullable so pre-Outcome rows backfill cleanly and the
|
|
/// bespoke stored-procedure audit path (which does not derive an outcome) writes NULL.</summary>
|
|
public string? Outcome { get; set; }
|
|
}
|