docs(acl): record the shared-root-folder browse-transit mapping
CLAUDE.md's "Browse needs ancestor visibility" bullet and docs/security.md's ancestor-visibility bullet now name the topmost ancestor: the single shared root folder (s=OtOpcUa, raw namespace) that both realm trees hang under, its mapping onto the trie's zero-segment realm-root scope, the either-realm transit (it fronts both trees), and the realm qualification. Also corrects the adjacent security.md bullet that claimed "the two namespace root folders remain visible" — there is one anchor, not one per namespace, and what an ungranted session sees is the ns=0 Objects listing of it rather than a browse into it. Flagged rather than silently left, since it contradicts the mechanism the new bullet documents.
This commit is contained in:
@@ -801,7 +801,13 @@ Two things to know before relying on any of this:
|
|||||||
grant deep in the tree is reached by browsing the nodes above it, and the trie inherits downward
|
grant deep in the tree is reached by browsing the nodes above it, and the trie inherits downward
|
||||||
only, so `PermissionTrieNode.BrowseTransitPrincipals` marks the path. Planting a real `Browse` grant
|
only, so `PermissionTrieNode.BrowseTransitPrincipals` marks the path. Planting a real `Browse` grant
|
||||||
on each ancestor instead would be collected by the additive downward walk and leak `Browse` onto
|
on each ancestor instead would be collected by the additive downward walk and leak `Browse` onto
|
||||||
every sibling subtree beneath that ancestor.
|
every sibling subtree beneath that ancestor. ⚠️ **The topmost ancestor is the single shared root
|
||||||
|
folder** (`s=OtOpcUa`, raw namespace) that BOTH realm trees hang under — its identifier is a display
|
||||||
|
name, not a path, so `DataPlaneAuthorizer.BuildScope` maps it onto the trie's **zero-segment
|
||||||
|
realm-root scope** (`V3NodeIds.SharedRootFolderId`, realm-qualified: in the UNS namespace that
|
||||||
|
identifier is an ordinary Area), and its Browse considers transit in **either** realm because it
|
||||||
|
fronts both trees. Before this every narrow Browse grant was unreachable by navigation — the Browse
|
||||||
|
gate denies the whole call when its source node is denied, so clients were stopped at the front door.
|
||||||
- **Revocation reaches live subscriptions** (stage 6): every other operation re-asks per request, but a
|
- **Revocation reaches live subscriptions** (stage 6): every other operation re-asks per request, but a
|
||||||
subscription is authorized once and then streams — and streaming is how machine consumers actually
|
subscription is authorized once and then streams — and streaming is how machine consumers actually
|
||||||
read this server. `DataPlaneAuthorityState.Changed` fires `RevalidateMonitoredItems`. ⚠️ A denied
|
read this server. `DataPlaneAuthorityState.Changed` fires `RevalidateMonitoredItems`. ⚠️ A denied
|
||||||
|
|||||||
+14
-3
@@ -427,12 +427,23 @@ Behaviours worth knowing before enabling it:
|
|||||||
what this necessarily discloses — the *names* of the folders on the path to a granted node are visible
|
what this necessarily discloses — the *names* of the folders on the path to a granted node are visible
|
||||||
to whoever holds that grant. It is conferred only by grants that actually carry `Browse`, and it
|
to whoever holds that grant. It is conferred only by grants that actually carry `Browse`, and it
|
||||||
confers nothing but `Browse`.
|
confers nothing but `Browse`.
|
||||||
|
- **…including the shared root folder every browse starts at.** Both realm trees hang under a single
|
||||||
|
anchor folder (`s=OtOpcUa`, minted in the **raw** namespace by `CreateAddressSpace`; top-level raw
|
||||||
|
folders and top-level UNS areas alike parent under it). Its identifier is a display name rather than
|
||||||
|
a path, so splitting it produced a segment no grant path can contain, and the Browse gate refuses the
|
||||||
|
*whole* call when its source node is denied — every narrow `Browse` grant was therefore unreachable
|
||||||
|
by navigation, denied at the front door. The gate maps that one identifier onto the trie's
|
||||||
|
zero-segment **realm-root** scope, and because the anchor fronts both trees its browse considers
|
||||||
|
transit in **either** realm: a principal whose only grant lives in the UNS tree still enters through
|
||||||
|
this raw-namespace node. The mapping is realm-qualified — in the UNS namespace `s=OtOpcUa` is an
|
||||||
|
ordinary Area an operator happened to name that way and is evaluated as the path it is.
|
||||||
- **A denied node reports `BadUserAccessDenied` rather than `BadNodeIdUnknown`**, because the gate runs
|
- **A denied node reports `BadUserAccessDenied` rather than `BadNodeIdUnknown`**, because the gate runs
|
||||||
before node resolution. That is the information-hiding direction — error codes can no longer be used
|
before node resolution. That is the information-hiding direction — error codes can no longer be used
|
||||||
to enumerate the address space — but it does mean "access denied" is not evidence the node exists.
|
to enumerate the address space — but it does mean "access denied" is not evidence the node exists.
|
||||||
- **The two namespace root folders remain visible** to any admitted session. They are referenced from
|
- **The shared root folder remains *listed*** for any admitted session. It is referenced from the ns=0
|
||||||
the ns=0 `Objects` folder, whose browse the SDK's own node manager serves; a session with no grants
|
`Objects` folder, whose browse the SDK's own node manager serves, so a session with no grants sees the
|
||||||
sees the two roots and is refused on entry.
|
anchor in that listing — and is refused the moment it tries to browse *into* it (the transit above is
|
||||||
|
what a granted session enters on). There is one anchor, not one per namespace.
|
||||||
|
|
||||||
#### Cost
|
#### Cost
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user