docs(security,core): correct stale write-outcome doc + note benign DraftSnapshot/LeaderChanged residue (stillpending §9/§3)
This commit is contained in:
+17
-1
@@ -327,9 +327,25 @@ The rule is intentionally scoped to async surfaces — pure in-memory accessors
|
||||
|
||||
---
|
||||
|
||||
## Write-Outcome Self-Correction
|
||||
|
||||
When an OPC UA client writes a value and the driver reports a failure, the server automatically
|
||||
reverts the variable node to its prior value and surfaces the Bad-quality status code to the client.
|
||||
This prevents the node from showing a phantom Good value after a device-level write error. The
|
||||
revert is local to the node that received the write — no cluster coordination is required.
|
||||
|
||||
Additionally, an `AuditWriteUpdateEvent` is emitted on every write attempt (success or failure),
|
||||
consistent with OPC UA Part 4 audit requirements. The event carries the source node id, the
|
||||
requested value, and the final status code so audit log consumers can trace write outcomes without
|
||||
polling the node.
|
||||
|
||||
(Implemented in master `1d797c1c`.)
|
||||
|
||||
---
|
||||
|
||||
## Audit Logging
|
||||
|
||||
- **Server**: authentication, certificate-validation, and write-denial events are logged through the regular Serilog rolling file sink.
|
||||
- **Server**: authentication, certificate-validation, write, and write-denial events are logged through the regular Serilog rolling file sink; write outcomes also emit an `AuditWriteUpdateEvent` (see [Write-Outcome Self-Correction](#write-outcome-self-correction)).
|
||||
- **Admin**: `AuditWriterActor` (`src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Audit/AuditWriterActor.cs`) writes `ConfigAuditLog` rows (`src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ConfigAuditLog.cs`) to the Config DB for publish, rollback, cluster-node CRUD, and credential rotation. Visible on the cluster Audit page (`ClusterAudit.razor`) for operators with `Viewer` or above.
|
||||
|
||||
---
|
||||
|
||||
@@ -191,7 +191,9 @@ public sealed class ClusterRoleInfo : IClusterRoleInfo, IDisposable
|
||||
{
|
||||
Receive<ClusterEvent.IMemberEvent>(e => owner.HandleMemberEvent(e));
|
||||
Receive<ClusterEvent.RoleLeaderChanged>(e => owner.HandleRoleLeaderChanged(e));
|
||||
Receive<ClusterEvent.LeaderChanged>(_ => { /* no-op for now; reserved for ServiceLevel calc */ });
|
||||
// LeaderChanged is intentionally a no-op here: ServiceLevel lives in RedundancyStateActor
|
||||
// (admin-role singleton) and has no consumer on this ClusterRoleInfo path by design.
|
||||
Receive<ClusterEvent.LeaderChanged>(_ => { });
|
||||
Receive<ClusterEvent.CurrentClusterState>(_ => { /* seeded from initial snapshot */ });
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,9 @@ public static class DraftSnapshotFactory
|
||||
public static async Task<DraftSnapshot> FromConfigDbAsync(OtOpcUaConfigDbContext db, CancellationToken ct = default)
|
||||
=> new DraftSnapshot
|
||||
{
|
||||
// GenerationId=0 and null Enterprise/Site are intentional conservative fallbacks at the
|
||||
// global-factory level: the path-length validator uses its upper bound, and cross-gen
|
||||
// EquipmentUuid checks run in separate validator passes — no rule here reads these fields.
|
||||
GenerationId = 0, // generation model dropped; placeholder (no rule reads it)
|
||||
ClusterId = string.Empty, // global snapshot; rules compare entity ClusterId fields, not this
|
||||
Namespaces = await db.Namespaces.AsNoTracking().ToListAsync(ct),
|
||||
@@ -42,7 +45,7 @@ public static class DraftSnapshotFactory
|
||||
Tags = await db.Tags.AsNoTracking().ToListAsync(ct),
|
||||
VirtualTags = await db.VirtualTags.AsNoTracking().ToListAsync(ct),
|
||||
PollGroups = await db.PollGroups.AsNoTracking().ToListAsync(ct),
|
||||
PriorEquipment = [],
|
||||
PriorEquipment = [], // intentional: no prior-generation table to diff against at this level
|
||||
ActiveReservations = await db.ExternalIdReservations
|
||||
.AsNoTracking()
|
||||
.Where(r => r.ReleasedAt == null) // active only — matches DraftSnapshot.ActiveReservations semantics
|
||||
|
||||
Reference in New Issue
Block a user