ACL: a NodeAcl row can scope at an entity in another cluster and pass the deploy gate #529

Closed
opened 2026-08-01 01:10:30 -04:00 by dohertj2 · 0 comments
Owner

Fixed in 3a6b2a1d (#520 stage 3). Never shipped — the deploy gate itself is new in that commit.

The new DraftValidator.ValidateNodeAclScopes rule initially checked only that a ScopeId resolved to a
real entity. A row in cluster A naming an entity in cluster B resolves to a real trie path, so it passed —
a cross-cluster authorization leak rather than the "names nothing" failure the rule was about.

Fix. New error code NodeAclScopeCrossCluster, distinct from NodeAclScopeUnresolved, because the two
need different remedies and the message names both clusters.

Ownership is non-uniform and worth recording: RawFolder, DriverInstance and UnsArea carry ClusterId
directly; Device, TagGroup, Tag, UnsLine and Equipment inherit it through their parent chain.
Neither self-nesting axis (ParentTagGroupId, ParentRawFolderId) lies on the path to the cluster, so every
chain is ≤ 3 hops and cannot loop — no depth guard needed, unlike AclScopePathResolver's ancestry walks.

The lookups are deliberately fleet-wide, not cluster-filtered: a cluster-filtered lookup would make a
cross-cluster id present as "unresolved" and the new rule would never fire. That trap is pinned by a
mutation test asserting the cross-cluster row is never also reported as unresolved.

**Fixed in `3a6b2a1d` (#520 stage 3). Never shipped — the deploy gate itself is new in that commit.** The new `DraftValidator.ValidateNodeAclScopes` rule initially checked only that a `ScopeId` resolved to a real entity. A row in cluster A naming an entity in cluster B resolves to a *real* trie path, so it passed — a cross-cluster authorization leak rather than the "names nothing" failure the rule was about. **Fix.** New error code `NodeAclScopeCrossCluster`, distinct from `NodeAclScopeUnresolved`, because the two need different remedies and the message names both clusters. Ownership is non-uniform and worth recording: `RawFolder`, `DriverInstance` and `UnsArea` carry `ClusterId` directly; `Device`, `TagGroup`, `Tag`, `UnsLine` and `Equipment` inherit it through their parent chain. Neither self-nesting axis (`ParentTagGroupId`, `ParentRawFolderId`) lies on the path to the cluster, so every chain is ≤ 3 hops and cannot loop — no depth guard needed, unlike `AclScopePathResolver`'s ancestry walks. The lookups are deliberately fleet-wide, not cluster-filtered: a cluster-filtered lookup would make a cross-cluster id present as "unresolved" and the new rule would never fire. That trap is pinned by a mutation test asserting the cross-cluster row is *never* also reported as unresolved.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#529