Merge #586: Server-object (i=2253) event items gated by the data-plane ACL; per-condition delivery with SDK audit guard + monitored bookkeeping
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -36,9 +36,12 @@ same condition is wired as an **event notifier of every referencing equipment fo
|
||||
`EnsureFolderIsEventNotifier(equipFolder)`.
|
||||
|
||||
- A **single** `ReportEvent` fans through the SDK notifier graph to the raw device folder,
|
||||
every referencing equipment folder, and up to the Server object. A subscriber at any one
|
||||
every referencing equipment folder, and to the Server object. A subscriber at any one
|
||||
root receives **exactly one** copy of the transition — the Part 9 shared-`InstanceStateSnapshot`
|
||||
dedup (`MonitoredItem.QueueEvent` → `IsEventContainedInQueue`), **never** one copy per root.
|
||||
The Server-object (`i=2253`) copy is delivered by the condition itself, not by the folders'
|
||||
forward to the core Server object, so it can be filtered per subscriber by the data-plane ACL
|
||||
(Gitea #586 — see `docs/security.md`).
|
||||
Duplicating the `ReportEvent` per root is rejected by design (distinct EventIds would break
|
||||
Server-object dedup + Part 9 ack correlation).
|
||||
- **Teardown is symmetric:** the node manager tracks the wired notifier pairs and calls
|
||||
|
||||
+45
-10
@@ -531,16 +531,51 @@ Behaviours worth knowing before enabling it:
|
||||
- **A client may delete a stood-down item** normally; the delete is absorbed (the item is already off
|
||||
the fan-out) and a later restoring deployment does not resurrect it.
|
||||
|
||||
⚠️ **Server-object (all-events) event subscriptions are outside the ACL entirely — at creation and at
|
||||
revalidation.** A subscription to `i=2253` (the Server object) is fanned out by the SDK through
|
||||
`SubscribeToAllEvents` and through the Server object's own notifier, which the core node manager owns —
|
||||
our node manager's creation gate never sees it, and an equipment folder with no direct subscriber
|
||||
forwards its events to the Server object regardless of what our node manager attaches. So with the flag
|
||||
on, a session subscribed to the Server object still receives every alarm in the address space (observed
|
||||
over the wire 2026-10-07: an anonymous session holding no grant at all received a native alarm through a
|
||||
Server-object event item). Scope
|
||||
alarm consumers by subscribing to the equipment/device folders, and treat Server-object event access as
|
||||
ungated until this is closed.
|
||||
**Server-object (`i=2253`, all-events) event subscriptions are filtered per event at delivery** (Gitea
|
||||
#586). They used to be outside the ACL entirely: the SDK attached such an item to the fan-out of every
|
||||
one of our root notifiers through `SubscribeToAllEvents`, and an equipment folder with no direct
|
||||
subscriber forwarded its events to the core node manager's Server object, which our node manager never
|
||||
sees — so a session holding no grant received every alarm in the server. Now:
|
||||
- The node manager attaches a Server-object item to **nothing**. It records it, and each condition hands
|
||||
its events to the recorded items itself (the condition's own `OnReportEvent`, which fires exactly once
|
||||
per event, so the Server object still sees one copy). The root-notifier forward to the core Server
|
||||
object is dropped.
|
||||
- Each event is delivered to an item only if the item's session holds `Subscribe` on **any** of: the
|
||||
condition itself (`ConditionId` = RawPath, raw realm — so a grant on the alarm's raw tag admits it), a
|
||||
root-notifier folder the condition bubbles through (its raw device folder; a scripted alarm's
|
||||
equipment folder), or an equipment folder wired as its extra notifier. That is the per-folder path's
|
||||
question — a session that may subscribe to any notifier the alarm reports through already receives it
|
||||
there — asked with the same authorizer and the same operation as the per-folder creation gate. The
|
||||
folders must be asked as well as the condition because the trie inherits downward only within a realm:
|
||||
an `Equipment` grant covers the UNS folder, not the condition at its RawPath.
|
||||
- **Revalidation is implicit, not trigger-driven.** Every event re-asks the authorizer, whose decision
|
||||
cache is keyed per generation and which re-checks key revocation and expiry on every call — so a
|
||||
deployment takes effect on the next event, and an expired key's feed stops on the next event, without
|
||||
waiting for the `AclRevalidationTimer` tick. Nothing is detached, so restoring a grant needs no
|
||||
re-attach: the next event simply passes. Events raised while withheld are not replayed, and the client
|
||||
is not told (the same reasoning as a stood-down direct item).
|
||||
- Creation is **not** refused — an all-events item is legitimate for a partially-granted session; what
|
||||
is withheld is each event it may not see.
|
||||
- **`ConditionRefresh` on a Server-object item** replays only the conditions the session passes the same
|
||||
check on (once per condition, not once per notifier it is wired to). Without that, a refresh would
|
||||
replay every condition's current state to any session.
|
||||
- Inert with the flag off: every Server-object item receives every condition, as before.
|
||||
- The replacement delivery keeps the two behaviours of the SDK path it replaced. **Audit events** a
|
||||
condition reports through itself (`AuditConditionAcknowledgeEventType`, Comment, Enable, Shelving)
|
||||
reach a Server-object item only while the server is auditing, over SignAndEncrypt or https-binary,
|
||||
which is exactly the SDK's own guard. And while any Server-object item exists, every condition counts as
|
||||
**monitored** (`ConditionState.EventsMonitored()`), so the SDK still reports its method-driven state
|
||||
changes on a server whose only subscribers are Server-object items. Both were caught in review: the
|
||||
first cut queued `i=8944` to every Server-object item with auditing off, and left conditions
|
||||
unmonitored. The second also needed the SDK's `AddRootNotifier` neutralised: its "subscribe existing
|
||||
all-events items with `unsubscribe: true`" bug would otherwise take the unit back from every
|
||||
condition rebuilt under a live item.
|
||||
|
||||
Residual, not closed by #586: **audit and model-change events** (`AuditWriteUpdateEvent`,
|
||||
`GeneralModelChangeEvent`, …) are still reported straight to the Server object and reach every
|
||||
Server-object item ungated. They are delivered only when auditing / model-change reporting is on (and
|
||||
audit events only over an encrypted channel), and a node manager has no seam on the core Server
|
||||
object's fan-out to filter them.
|
||||
- **An ancestor of a granted node is browsable**, or no narrow grant would be reachable: a client
|
||||
arrives at a deep node by browsing its ancestors, and the trie otherwise inherits downward only. Note
|
||||
what this necessarily discloses — the *names* of the folders on the path to a granted node are visible
|
||||
|
||||
Reference in New Issue
Block a user