docs+ui(security): state plainly that node ACLs are not enforced (§8.1, #520)

deferment.md §8.1 asked for a decision: wire IPermissionEvaluator into the node
manager, or say plainly that ACLs are not enforced. Decision: the latter. The
evaluator stays in the tree; #520 tracks the wire-up.

Verifying the claim turned up four things worse than the audit recorded:

- docs/security.md, not ReadWriteOperations.md, was the highest-risk doc. It
  claimed an anonymous session is "default-denie[d] any node a session has no
  ACL grant for". The opposite holds: anonymous can Browse/Read/Subscribe/
  HistoryRead the entire address space, and is refused writes and alarm acks
  only because it carries no roles.
- Three of five documented data-plane role strings do nothing.
  OpcUaDataPlaneRoles declares exactly WriteOperate and AlarmAck; ReadOnly,
  WriteTune and WriteConfigure are compared against nowhere in src/. The doc
  called all five "exact, case-insensitive, and code-true".
- ReadWriteOperations.md was fiction beyond the ACL claims. OnReadValue has
  zero occurrences in src/ — the documented read path did not exist. Reads
  never reach a driver: the node manager is push-model and the SDK serves a
  Read from the cached pushed value. WriteAuthzPolicy, _sourceByFullRef,
  _writeIdempotentByFullRef and IRoleBearer are likewise zero-hit, and
  CapabilityInvoker is not referenced by the OpcUaServer project at all. This
  file is rewritten rather than bannered.
- v2-release-readiness.md claimed a whole enforcement layer shipped —
  FilterBrowseReferences, GateCallMethodRequests, MapCallOperation,
  AuthorizationBootstrap, Node:Authorization:* keys. None exist. Whatever
  landed on the v2 branch did not survive into the shipped tree.

The UI change is the operative one: ClusterAcls.razor and AclEdit.razor now
warn that a grant is saved and shipped in every deployment artifact but never
evaluated, so an operator cannot author a deny rule believing it works.

deferment.md gains a §9 execution log tracking §8 remediation.
This commit is contained in:
Joseph Doherty
2026-07-27 18:36:43 -04:00
parent e08855fb9d
commit 53ede679c3
7 changed files with 336 additions and 93 deletions
+63 -2
View File
@@ -342,9 +342,11 @@ appear in §7.1 as well.
## 8. Recommended sequencing
1. **§3.1 ACL enforcement** — decide: wire `IPermissionEvaluator` into `OtOpcUaNodeManager`, or
1. ~~**§3.1 ACL enforcement** — decide: wire `IPermissionEvaluator` into `OtOpcUaNodeManager`, or
remove the authoring UI and say plainly that ACLs are not enforced. Either way fix
`docs/ReadWriteOperations.md` first; it is the one that could mislead a security review.
`docs/ReadWriteOperations.md` first; it is the one that could mislead a security review.~~
**Done** — decided *make non-enforcement explicit*; wire-up tracked as Gitea **#520**. See §9.
(`docs/ReadWriteOperations.md` was **not** the sharpest one — `docs/security.md` was.)
2. **#518 + #507 together** — neither fix is observable alone.
3. **#516** — silent config discard on 5 drivers; then re-scope #489, which it partly subsumes.
4. **G-4** — extend the existing picker-parity test to `DriverConfigModal` + `DeviceModal`; it would
@@ -356,6 +358,65 @@ appear in §7.1 as well.
---
## 9. Execution log
Remediation of §8, on branch `feat/deferment-remediation`. Status is updated in the **same commit** as
the work, so this register never disagrees with the tree.
| §8 item | Status | Commit |
|---|---|---|
| 1. ACL enforcement decision | ✅ **Done** — decided *make non-enforcement explicit*; Gitea **#520** tracks the wire-up | `d1e88dc4` |
| 2. #518 + #507 | ⏳ Not started — decided *signal, not mutation* | — |
| 3. #516 config discard | ⏳ Not started — decided *both* (per-driver re-parse **and** seam respawn) | — |
| 4. G-4 dispatch-map parity | ⏳ Not started | — |
| 5. Bookkeeping sweep | ⏳ Not started | — |
| 6. Tier truth | ⏳ Not started — decided *document the truth, defer the code decision* | — |
### 2026-07-27 — §8.1 ACL non-enforcement made explicit
Decision: **do not wire the evaluator, do not delete it** — state plainly that it does not run, and
track the wire-up as **Gitea #520**.
Applied:
- `docs/security.md` — the `NodeScope`/`PermissionTrie`/"Dispatch gate" block now leads with a
**What is enforced today** table (the three real checks) before the design material, which is
labelled *Designed but not wired*.
- `docs/ReadWriteOperations.md`**rewritten**, not bannered.
- `docs/v2/acl-design.md` — ⚠️ NEVER WIRED banner.
- `docs/v2/v2-release-readiness.md` §"Security — Phase 6.2 dispatch wiring" — the CLOSED claim marked
as not holding.
- `ClusterAcls.razor` + `AclEdit.razor` — warning alerts stating rules are stored and deployed but
never evaluated.
**Four things this pass found that §7 and the original audit missed** — all worse than what was
already recorded:
1. **`docs/security.md` was the highest-risk doc, not `ReadWriteOperations.md`.** `:115` claimed an
anonymous session is "default-denie[d] any node a session has no ACL grant for". The opposite is
true: an anonymous session can Browse, Read, Subscribe and HistoryRead the **entire** address
space. It is refused only writes and alarm acks — and only because it carries no roles.
2. **Three of the five documented data-plane role strings do nothing.** `OpcUaDataPlaneRoles` declares
exactly `WriteOperate` and `AlarmAck`. `ReadOnly`, `WriteTune` and `WriteConfigure` are compared
against nowhere in `src/`, so mapping a group to `WriteTune` grants nothing and mapping one to
`ReadOnly` restricts nothing. The doc called all five "exact, case-insensitive, and **code-true**".
3. **`ReadWriteOperations.md` was fiction well beyond the ACL claims.** `OnReadValue` has **zero**
occurrences in `src/` — the whole documented read path did not exist. Reads never reach a driver at
all: the node manager is push-model and the SDK serves a client Read from the cached pushed value.
`WriteAuthzPolicy`, `_sourceByFullRef`, `_writeIdempotentByFullRef` and `IRoleBearer` are likewise
zero-hit, and `CapabilityInvoker` is not referenced by the `OpcUaServer` project at all.
4. **`v2-release-readiness.md` claimed a whole enforcement layer shipped.** Beyond the `:41` row §7.1
already flagged, `:46-50` describe `FilterBrowseReferences`, `GateCallMethodRequests`,
`MapCallOperation`, `AuthorizationBootstrap` and `Node:Authorization:*` config keys as Closed or
Partial. None of them exist. Whatever landed on the v2 branch did not survive into the shipped tree.
Also recorded in #520 and not previously known: every existing evaluator unit test runs in
`PermissionTrieBuilder`'s no-`scopePaths` *deterministic test mode*, so the production hierarchy path
is effectively untested; and `NodeAcl.ScopeId` has no FK or existence check, so scope ids dangle
silently.
---
*Generated 2026-07-27 against `master` @ `90bdaa44` by five parallel source-verification agents.
Every `path:line` reference was read from source. Items marked CANNOT VERIFY require hardware, a
VPN'd gateway, or a live rig and are listed as unproven rather than assumed.*