fix(acl): filter Server-object (i=2253) event subscriptions per event through the data-plane ACL (#586)
Server-object (all-events) items bypassed the ACL entirely: the SDK attached them to every root notifier's fan-out via SubscribeToAllEvents, and a root notifier with no direct subscriber forwarded to the core Server object, which this node manager never sees. - SubscribeToAllEvents override records the item and attaches it to nothing. - Each condition's own OnReportEvent (fires once per event) queues the event to each recorded item whose session holds Subscribe on the condition (RawPath) or any notifier folder it reports through -- the per-folder path's question; otherwise mirrors MonitoredNode2.OnReportEvent. - The root-notifier forward to the core Server object (OnReportEvent override) is dropped. - ConditionRefresh on an i=2253 item is filtered the same way (once per condition). - Revalidation is implicit: every event re-asks the authorizer (per-generation cache, key expiry and revocation re-checked per call). Inert with Security:Acl:Enabled=false. Over-the-wire tests: no-grant vs granted (positive control), created-before-deploy, revoke/restore by deployment, key expiry with no tick, refresh, and flag-off. Falsified: disabling the filter, or restoring the forward, turns the enforcement tests red. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+35
-10
@@ -531,16 +531,41 @@ 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.
|
||||
|
||||
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