fix(opcua): drop the reflective folder-event verdict invalidation; deny the SDK channel under enforcement (#878)
v2-ci / unit-tests (push) Waiting to run

The SDK memoizes INodeManager3.ValidateEventRolePermissions per (item, event type,
SourceNode). #826 kept it fresh by reflecting into the private
MonitoredNode2.InvalidatePermissionCaches (plus a detach/re-attach fallback). Since #855
condition events never take that channel, and the search for #878 found nothing
production-raised that does except a condition's SDK method audits, which reach it only
with auditing on, SignAndEncrypt and a SecurityAdmin session (nothing grants that role).

Under enforcement the channel verdict is now a flat deny for items on our nodes, so no
cached verdict depends on a grant, and InvalidateFolderEventVerdicts, the reflection,
the fallback, its counter and the guard test are removed. Method audits still reach
Server-object items, decided per event.

Tests: FolderEventVerdictInvalidationTests -> FolderEventVerdictFreshnessTests; new
revoked-grant test (positive control: a still-granted session) and method-audit test
(SecurityAdmin mapped for the test; positive controls: Server-object item, flag off);
the plain-event test runs each mode on a fresh server. Mutation-checked.

Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
This commit is contained in:
Joseph Doherty
2026-11-12 11:38:11 -05:00
parent 3a312ffdd1
commit fa811feb3d
5 changed files with 414 additions and 394 deletions
+30 -13
View File
@@ -823,13 +823,28 @@ Behaviours worth knowing before enabling it:
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 clears each notifier's cache whole (sessions and orphaned, session-less subscriptions alike — an orphaned
subscription must not queue a moved alarm's events for a later `TransferSubscriptions`) on every new generation or
authorizer, on each `AclRevalidationTimer` tick, and at the end of every apply, and not at all while enforcement is
off. A verdict is decided on the condition currently live under the event's NodeId, so a replaced instance's late
event cannot leave a deny behind for its successor. Only alarm-bearing raw tags are carried in the reference map. Withheld events are not replayed and the client is not told; restoring the reference or
same way. Condition events are decided **per event, per item, uncached** by the node manager's own delivery
(`DeliverToNotifierItems`, #855), so a deploy, a revoked grant or a dropped reference decides the very next event —
for orphaned, session-less subscriptions too (an orphaned subscription must not queue a moved alarm's events for a
later `TransferSubscriptions`). A verdict is decided on the condition currently live under the event's NodeId, so a
replaced instance's late event is treated exactly as the live one's.
**No SDK verdict cache to invalidate (Gitea #878).** Whatever still reaches one of our folders through the SDK's
own `MonitoredNode2` channel is judged by `INodeManager3.ValidateEventRolePermissions` (re-implemented — the base
method is not virtual), and the SDK **memoizes** that verdict per `(item, event type, SourceNode)` until the
monitored node's permission generation moves — not when a deployment or grant changes. #826 kept it fresh by calling
the SDK's *private* `MonitoredNode2.InvalidatePermissionCaches` by reflection (with a detach/re-attach fallback).
#878 removed that: under enforcement every event on that channel is **withheld** from items on our nodes, so the
cached verdict is a constant deny that no grant can make stale. What rides the channel (SDK 2.0.1, decompiled): only a
condition's SDK-raised Part 9 method audit events — and those reach it only with auditing on
(`ServerConfiguration.AuditingEnabled`, never set here), over SignAndEncrypt, to a session holding `SecurityAdmin`,
the one well-known role the SDK's type check admits to audit types, which the SDK's default `RoleManager` maps to no
identity and this server never grants — plus any event a `NodeState.ReportEvent` on one of our folders would raise
(none is raised). Condition events never take the channel (#855), and the SDK's own condition reports stay off
(`AutoReportStateChanges = false`). A condition's method audits still reach **Server-object** items, decided per
event. Residual: a verdict cached while enforcement was **off** survives the switch to on; the switch happens once,
when the host wires the authorizer just after the server starts listening, and the only allow it could carry is a
method audit to a `SecurityAdmin` session. Only alarm-bearing raw tags are carried in the reference map. 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.
@@ -846,12 +861,13 @@ Behaviours worth knowing before enabling it:
folder it legitimately notifies through for the session), which in this ACL model is the deliberate
generalisation of "the source": a `Tag` grant covers a native alarm's raw node and every UNS projection of it, and
an `Equipment` grant covers the alarms it owns or references.
- **A condition's Part 9 method audit events** (Acknowledge, Comment, Shelve…) — SourceNode is the condition, so they
are resolved to it and follow exactly the condition rule (plus the SDK's audit/SignAndEncrypt guard on the
Server-object path).
- **A condition's Part 9 method audit events** (Acknowledge, Comment, Shelve…) — SourceNode is the condition. At the
Server object they are resolved to it and follow exactly the condition rule (plus the SDK's audit/SignAndEncrypt
guard). At a folder item they travel the SDK's channel and are **withheld** under enforcement (Gitea #878, see
above) — stricter than Part 3, which it permits; only a `SecurityAdmin` session could receive them at all.
- **Any other event on a folder item** (none is produced today; `GeneralModelChangeEvent` and the rejected-write
`AuditWriteUpdateEvent` go to the Server object, #588) names no live condition and is **withheld** under
enforcement — fail closed, which is stricter than the folder's permission and never laxer than Part 3.
`AuditWriteUpdateEvent` go to the Server object, #588) is **withheld** under enforcement — fail closed, which is
stricter than the folder's permission and never laxer than Part 3.
With the flag off only the SDK's own type + SourceNode `ReceiveEvents` check applies, as before.
**Event bursts never block under the node-manager lock (Gitea #838).** Each notifier with event items has an SDK
@@ -925,7 +941,8 @@ Behaviours worth knowing before enabling it:
type/source `ReceiveEvents` + the #826 decision under enforcement — its item-independent part, the live condition,
the notifier check and the visibility scopes, resolved once per hop) **per event, uncached**, then the item's gate and
cut, then `QueueEvent`. The `MonitoredNode2` stays for item attachment and for what still uses its channel (a
condition's SDK method audit events, data-change items on the folder), so `InvalidateFolderEventVerdicts` stays.
condition's SDK method audit events, data-change items on the folder). (#878 later withheld everything on that
channel from our folder items under enforcement and removed `InvalidateFolderEventVerdicts`, see above.)
- **No notifier of ours defers any more.** `QueueEvent` never waits for a consumer (the item queue discards oldest at
`QueueSize`) and the verdict runs on the reporter's thread, which already holds `Lock` — so a burst under one
`Lock` hold completes with nothing deferred, and the #838 deadlock shape is gone for condition events. Folder items