Node ACLs are authored and deployed but never enforced — wire IPermissionEvaluator or retire the surface #520

Open
opened 2026-07-27 18:35:58 -04:00 by dohertj2 · 0 comments
Owner

Split out of the deferment.md §3.1 audit. Decision taken 2026-07-27: make the non-enforcement explicit now (docs + UI warnings — done), and track the real wire-up here.

What is true today

  • IPermissionEvaluator, TriePermissionEvaluator, PermissionTrieCache, PermissionTrieBuilder live in src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/ with 30 passing unit tests.
  • IPermissionEvaluator.Authorize has zero production call sites. Nothing registers it in DI.
  • ZB.MOM.WW.OtOpcUa.OpcUaServer.csproj does not reference ZB.MOM.WW.OtOpcUa.Core at all — the evaluator is not even on the node manager's compile-time reference graph.
  • Operators can author NodeAcl rows (/clusters/{id}/acls), and ConfigComposer snapshots every row into every deployment artifact — so an ACL edit shifts the artifact RevisionHash and ships fleet-wide.
  • DeploymentArtifact never deserializes them. The bytes arrive and are dropped.

Net: an operator can author a grant, deploy it green, and have it change nothing.

Actual enforcement is two server-wide role checks — WriteOperate (Value writes, either realm) and AlarmAck (all five Part 9 alarm methods). Read, Browse, HistoryRead and CreateMonitoredItems have no authorization check at all, so any admitted session including Anonymous reads the whole address space.

Four blockers — none of them is "call the evaluator"

  1. Raw-realm nodes have no representable scope. NodeHierarchyKind has exactly one member, Equipment. Raw NodeIds are Folder/Driver/Device/TagGroup/Tag RawPaths, which NodeScope cannot express — yet the write gate is attached to raw tags. Roughly half the writable address space has no scope to evaluate. This is a design decision, not wiring.
  2. The session carries roles, not LDAP groups. The trie matches NodeAcl.LdapGroup, but OpcUaUserAuthResult carries only mapped role strings; the group list is discarded inside LdapOpcUaUserAuthenticator.
  3. PermissionTrieBuilder's scopePaths argument has no producer anywhere in the tree. Building without it drops every sub-cluster grant into the root-child fallback — the hazard the builder's own comments warn about. Note every existing evaluator unit test runs in the no-scopePaths "deterministic test mode", so the production hierarchy path is effectively untested.
  4. No session lifecycle exists. UserAuthorizationState has the freshness/staleness predicates but nothing constructs one, stamps AuthGenerationId, or drives a refresh.

Plus: the node side never parses ACLs out of the artifact, and OtOpcUaNodeManager has no cluster identity (NodeScope.ClusterId is required and the evaluator fails closed on mismatch).

Smaller inconsistencies inside the subsystem

  • NodeAclScopeKind.FolderSegment is offered in the authoring dropdown but the trie walker has no level to match it at.
  • NodePermissions.AlarmRead has no OpcUaOperation mapping — grantable but unreachable.
  • NodeAcl.ScopeId has no FK and no existence check, so scope ids can dangle silently.
  • The SystemPlatform hierarchy kind that acl-design.md describes was retired with v3.

Scope of a fix

Write has a real chokepoint already (OnEquipmentTagWrite) — it runs under the node-manager Lock, so the check must stay non-blocking; the evaluator is pure, so that fits. Read has no seam at all — it needs either a Read override or a per-variable OnSimpleReadValue handler on the hot publish path.

Already done

docs/security.md, docs/ReadWriteOperations.md, docs/v2/acl-design.md and docs/v2/v2-release-readiness.md corrected; warning banners added to ClusterAcls.razor and AclEdit.razor. v2-release-readiness.md had marked this "CLOSED 2026-04-19, PR #94" with a detailed description of Browse/CreateMonitoredItems/Call gating — none of those types exist in src/.

Split out of the `deferment.md` §3.1 audit. **Decision taken 2026-07-27: make the non-enforcement explicit now (docs + UI warnings — done), and track the real wire-up here.** ## What is true today - `IPermissionEvaluator`, `TriePermissionEvaluator`, `PermissionTrieCache`, `PermissionTrieBuilder` live in `src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/` with 30 passing unit tests. - `IPermissionEvaluator.Authorize` has **zero production call sites**. Nothing registers it in DI. - `ZB.MOM.WW.OtOpcUa.OpcUaServer.csproj` does not reference `ZB.MOM.WW.OtOpcUa.Core` at all — the evaluator is not even on the node manager's compile-time reference graph. - Operators **can** author `NodeAcl` rows (`/clusters/{id}/acls`), and `ConfigComposer` snapshots every row into every deployment artifact — so an ACL edit shifts the artifact `RevisionHash` and ships fleet-wide. - `DeploymentArtifact` never deserializes them. The bytes arrive and are dropped. Net: an operator can author a grant, deploy it green, and have it change nothing. Actual enforcement is two server-wide role checks — `WriteOperate` (Value writes, either realm) and `AlarmAck` (all five Part 9 alarm methods). **Read, Browse, HistoryRead and CreateMonitoredItems have no authorization check at all**, so any admitted session including Anonymous reads the whole address space. ## Four blockers — none of them is "call the evaluator" 1. **Raw-realm nodes have no representable scope.** `NodeHierarchyKind` has exactly one member, `Equipment`. Raw NodeIds are `Folder/Driver/Device/TagGroup/Tag` RawPaths, which `NodeScope` cannot express — yet the write gate is attached to raw tags. Roughly half the writable address space has no scope to evaluate. **This is a design decision, not wiring.** 2. **The session carries roles, not LDAP groups.** The trie matches `NodeAcl.LdapGroup`, but `OpcUaUserAuthResult` carries only mapped role strings; the group list is discarded inside `LdapOpcUaUserAuthenticator`. 3. **`PermissionTrieBuilder`'s `scopePaths` argument has no producer** anywhere in the tree. Building without it drops every sub-cluster grant into the root-child fallback — the hazard the builder's own comments warn about. Note every existing evaluator unit test runs in the no-`scopePaths` "deterministic test mode", so the production hierarchy path is effectively untested. 4. **No session lifecycle exists.** `UserAuthorizationState` has the freshness/staleness predicates but nothing constructs one, stamps `AuthGenerationId`, or drives a refresh. Plus: the node side never parses ACLs out of the artifact, and `OtOpcUaNodeManager` has no cluster identity (`NodeScope.ClusterId` is `required` and the evaluator fails closed on mismatch). ## Smaller inconsistencies inside the subsystem - `NodeAclScopeKind.FolderSegment` is offered in the authoring dropdown but the trie walker has no level to match it at. - `NodePermissions.AlarmRead` has no `OpcUaOperation` mapping — grantable but unreachable. - `NodeAcl.ScopeId` has no FK and no existence check, so scope ids can dangle silently. - The `SystemPlatform` hierarchy kind that `acl-design.md` describes was retired with v3. ## Scope of a fix Write has a real chokepoint already (`OnEquipmentTagWrite`) — it runs under the node-manager `Lock`, so the check must stay non-blocking; the evaluator is pure, so that fits. **Read has no seam at all** — it needs either a `Read` override or a per-variable `OnSimpleReadValue` handler on the hot publish path. ## Already done `docs/security.md`, `docs/ReadWriteOperations.md`, `docs/v2/acl-design.md` and `docs/v2/v2-release-readiness.md` corrected; warning banners added to `ClusterAcls.razor` and `AclEdit.razor`. `v2-release-readiness.md` had marked this "CLOSED 2026-04-19, PR #94" with a detailed description of Browse/CreateMonitoredItems/Call gating — none of those types exist in `src/`.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#520