fix(acl): per-event visibility for direct folder event items; generation-checked native wiring (#826)

Event items on an equipment / raw device folder were fed by the SDK's MonitoredNode2 fan-out with no ACL
decision, so after a deploy moved a scripted alarm (or dropped the reference projecting a native alarm into
an equipment) the OLD folder's subscribers kept receiving it - EventId included - until the rebuild landed,
indefinitely after a failed apply.

- OtOpcUaNodeManager re-implements INodeManager3 so ValidateEventRolePermissions (the SDK's per-event,
  per-item hook; base not virtual) adds IsNotifierEventVisibleTo: live condition instance, notifier still a
  legitimate notifier of it, and the #586/#808 Server-object rule (IsConditionVisibleTo over
  ConditionVisibilityScopes).
- The SDK memoizes that verdict per (item, event type, SourceNode): InvalidateFolderEventVerdicts runs on the
  authority/authorizer setters, RevalidateMonitoredItems (deploy + timer) and CompleteAddressSpaceApply.
- ConditionRefresh on direct items goes through RefreshDirectItems (base loop + the same decision).
- New generation field TagReferencingEquipment (RawPath -> EquipmentIds, from the artifact's
  UnsTagReferences via AclScopePathResolver.ResolveTagRawPath): a native alarm's wired equipment folder only
  counts (folder delivery AND Server-object scopes) while the current generation still references it.
- Secondary #808 edge: UNS variables wired UNS->Raw Organizes by AddReference are tracked and always placed
  as reference variables (an Area named like its equipment's id no longer narrows them).

Tests: UnsFolderScopeWireTests (moved alarm on a folder item + ConditionRefresh + rebuild; area named like
equipment id), NativeAlarmFolderEventAclWireTests (dropped reference: folder + Server object + refresh +
restore), DeploymentArtifactScopePathTests (projection). Rig/event tests now supply tag references.
This commit is contained in:
Joseph Doherty
2026-11-11 01:39:06 -05:00
parent 5bbe1e3c7a
commit 8e1e73f93c
13 changed files with 666 additions and 37 deletions
+28 -2
View File
@@ -600,9 +600,13 @@ and Write under old and new names until the address space re-points them. An id
folder / virtual tag / condition, a probe, a NodeDeleted model change) is decided only by a `Cluster` or `uns`
`Namespace` grant — so narrow grantees do not receive NodeDeleted for removed opaque-id nodes, and a removed id can
never match an unrelated narrow grant that shares its name; a four-segment (reference-shaped) id keeps its split.
Identifiers containing NUL are refused. Server-object (`i=2253`) delivery of a
Identifiers containing NUL are refused. A UNS variable the reference pass wired to its raw node (the `Organizes`
edge) is placed as a reference variable whatever its id spells (Gitea #826 — the id rule alone made an equipment's
references folder-owned when an Area was named exactly like that equipment's id, losing their `Tag`-grant match).
Server-object (`i=2253`) delivery of a
**scripted** condition is now decided on the condition alone (its folders add nothing in steady state and would
leak a moved alarm to its old owner). So a key holding only an `Equipment` grant can navigate to its equipment,
leak a moved alarm to its old owner). Direct event items on a folder get the same per-event decision (Gitea #826,
below). So a key holding only an `Equipment` grant can navigate to its equipment,
read its alarm children, receive its scripted alarm events through the Server object and Acknowledge / Confirm /
AddComment with the EventId it received. Fail-closed throughout: an unplaceable node is denied under every grant.
| HistoryRead (all four variants) | **nothing** | `HistoryRead` on each node's scope, decided **per node in the batch** |
@@ -709,6 +713,28 @@ Behaviours worth knowing before enabling it:
condition) runs on removal, rebuild and Server-object subscribe/delete. A deploy of N alarms under a
live Server-object item is therefore N bookkeeping steps, not N²/2 under the node-manager lock.
**Direct (per-folder) event items are filtered per event too** (Gitea #826). An item on an equipment or raw
device folder is fed by the SDK's own per-notifier fan-out (`MonitoredNode2`), which the #586 dispatcher never
touches — so after a deployment moved a scripted alarm (or dropped the reference projecting a native alarm into
an equipment), the OLD folder's subscribers kept receiving it, EventId included, until the rebuild landed, and
indefinitely after a failed apply (commands were already denied). With the flag **on**, each event to such an
item now also needs, against the CURRENT state:
- the event's condition to be the **live** instance (a torn-down one is withheld);
- the folder to still be a legitimate notifier of it — a node it hangs beneath, or a folder it is wired into
**that the current generation still projects its raw tag into** (`DataPlaneAuthoritySnapshot.TagReferencingEquipment`,
`RawPath → EquipmentIds`, decoded from the artifact's `UnsTagReferences` by the same resolver as the grants);
- and the condition to pass exactly the Server-object rule above for the item's session.
The generation check also narrows the **Server-object** rule: a wired folder whose reference the generation
dropped no longer admits the native alarm there either. **`ConditionRefresh`** on a folder item is filtered the
same way. The hook is `INodeManager3.ValidateEventRolePermissions`, which the node manager re-implements (the
base method is not virtual); the SDK memoizes that verdict per `(item, event type, SourceNode)`, so the node
manager invalidates it on every new generation or authorizer, on each `AclRevalidationTimer` tick, and at the
end of every apply. Withheld events are not replayed and the client is not told; restoring the reference or
grant resumes the feed on the next event. ⚠️ A native alarm reaches an equipment folder's items under
enforcement only while the installed generation carries that reference — a hand-built authority (tests) must
supply `tagReferencingEquipment`. Inert with the flag off.
**Model-change and audit events on the Server object are gated too** (Gitea #588). The node manager's
own server-wide events — `GeneralModelChangeEvent` (NodeAdded / NodeDeleted / DataTypeChanged) and the
`AuditWriteUpdateEvent` of a rejected device write — used to go straight to `Server.ReportEvent`, where