Phase 6.2 Stream B - Permission-trie evaluator (Core.Authorization) #85

Merged
dohertj2 merged 1 commits from phase-6-2-stream-b-permission-trie into v2 2026-04-19 09:29:53 -04:00
Owner

Ships the data-plane authorization engine Phase 6.2 runs on. Stream C wires it into OPC UA dispatch in a follow-up PR on this branch.

Summary

  • Abstractions: OpcUaOperation enum (Browse, Read, Write{Operate/Tune/Configure}, HistoryRead, HistoryUpdate, CreateMonitoredItems, TransferSubscriptions, Call, AlarmAck/Confirm/Shelve).
  • NodeScope addresses a node in the 6-level hierarchy; Kind selector switches Equipment (UNS) vs SystemPlatform (Galaxy, folder-segment walk).
  • AuthorizationDecision tri-state {Allow, NotGranted, Denied} + MatchedGrant provenance per decision #149. Phase 6.2 only produces Allow+NotGranted; Denied reserved for v2.1.
  • PermissionTrie + PermissionTrieBuilder.Build: pure additive union walk. Cluster-level grant cascades; equipment-level doesn’t leak to siblings; Galaxy folder-segment ditto; cross-cluster rows filtered.
  • PermissionTrieCache: process-singleton keyed on (ClusterId, GenerationId). Generation-sealed per decision #148. Out-of-order Install doesn’t downgrade current.
  • UserAuthorizationState: bounded MembershipFreshnessInterval 15 min (#151) + AuthCacheMaxStaleness 5 min (#152).
  • TriePermissionEvaluator: default IPermissionEvaluator impl. Fails closed on stale session, cross-cluster request, or empty cache. Maps every OpcUaOperation to its required NodePermissions flag.

Test plan

  • 27 new unit tests across 4 files exercising all B.6 invariants (cluster cascade, sibling isolation, multi-group union, no-grant deny, Galaxy folder isolation, cross-cluster filter, build idempotence) + evaluator semantics (HistoryRead needs its own bit, stale fail-closed, map totality) + cache semantics (current-generation pointer, out-of-order install, prune retention, cluster isolation).
  • Full solution dotnet test: 1078 passing (Phase 6.1 = 1042, 6.2A = +9, 6.2B = +27).
  • Stream C follow-up: wire DriverNodeManager Read/Write/HistoryRead/Subscribe/Browse/Call through the evaluator; needs scopePaths lookup from the live Configuration DB.

🤖 Generated with Claude Code

Ships the data-plane authorization engine Phase 6.2 runs on. Stream C wires it into OPC UA dispatch in a follow-up PR on this branch. ## Summary - **Abstractions**: `OpcUaOperation` enum (Browse, Read, Write{Operate/Tune/Configure}, HistoryRead, HistoryUpdate, CreateMonitoredItems, TransferSubscriptions, Call, AlarmAck/Confirm/Shelve). - **NodeScope** addresses a node in the 6-level hierarchy; `Kind` selector switches Equipment (UNS) vs SystemPlatform (Galaxy, folder-segment walk). - **AuthorizationDecision** tri-state {Allow, NotGranted, Denied} + MatchedGrant provenance per decision #149. Phase 6.2 only produces Allow+NotGranted; Denied reserved for v2.1. - **PermissionTrie** + `PermissionTrieBuilder.Build`: pure additive union walk. Cluster-level grant cascades; equipment-level doesn’t leak to siblings; Galaxy folder-segment ditto; cross-cluster rows filtered. - **PermissionTrieCache**: process-singleton keyed on (ClusterId, GenerationId). Generation-sealed per decision #148. Out-of-order Install doesn’t downgrade current. - **UserAuthorizationState**: bounded `MembershipFreshnessInterval` 15 min (#151) + `AuthCacheMaxStaleness` 5 min (#152). - **TriePermissionEvaluator**: default IPermissionEvaluator impl. Fails closed on stale session, cross-cluster request, or empty cache. Maps every `OpcUaOperation` to its required `NodePermissions` flag. ## Test plan - [x] 27 new unit tests across 4 files exercising all B.6 invariants (cluster cascade, sibling isolation, multi-group union, no-grant deny, Galaxy folder isolation, cross-cluster filter, build idempotence) + evaluator semantics (HistoryRead needs its own bit, stale fail-closed, map totality) + cache semantics (current-generation pointer, out-of-order install, prune retention, cluster isolation). - [x] Full solution `dotnet test`: 1078 passing (Phase 6.1 = 1042, 6.2A = +9, 6.2B = +27). - [ ] Stream C follow-up: wire DriverNodeManager Read/Write/HistoryRead/Subscribe/Browse/Call through the evaluator; needs scopePaths lookup from the live Configuration DB. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
dohertj2 added 1 commit 2026-04-19 09:29:43 -04:00
Ships Stream B.1-B.6 — the data-plane authorization engine Phase 6.2 runs on.
Integration into OPC UA dispatch (Stream C — Read / Write / HistoryRead /
Subscribe / Browse / Call etc.) is the next PR on this branch.

New Core.Abstractions:
- OpcUaOperation enum enumerates every OPC UA surface the evaluator gates:
  Browse, Read, WriteOperate/Tune/Configure (split by SecurityClassification),
  HistoryRead, HistoryUpdate, CreateMonitoredItems, TransferSubscriptions,
  Call, AlarmAcknowledge/Confirm/Shelve. Stream C maps each one back to its
  dispatch call site.

New Core.Authorization namespace:
- NodeScope record + NodeHierarchyKind — 6-level scope addressing for
  Equipment-kind (UNS) namespaces, folder-segment walk for SystemPlatform-kind
  (Galaxy). NodeScope carries a Kind selector so the evaluator knows which
  hierarchy to descend.
- AuthorizationDecision { Verdict, Provenance } + AuthorizationVerdict
  {Allow, NotGranted, Denied} + MatchedGrant. Tri-state per decision #149;
  Phase 6.2 only produces Allow + NotGranted, Denied stays reserved for v2.1
  Explicit Deny without API break.
- IPermissionEvaluator.Authorize(session, operation, scope).
- PermissionTrie + PermissionTrieNode + TrieGrant. In-memory trie keyed on
  the ACL scope hierarchy. CollectMatches walks Cluster → Namespace →
  UnsArea → UnsLine → Equipment → Tag (or → FolderSegment(s) → Tag on
  Galaxy). Pure additive union — matches that share an LDAP group with the
  session contribute flags; OR across levels.
- PermissionTrieBuilder static factory. Build(clusterId, generationId, rows,
  scopePaths?) returns a trie for one generation. Cross-cluster rows are
  filtered out so the trie is cluster-coherent. Stream C follow-up wires a
  real scopePaths lookup from the live DB; tests supply hand-built paths.
- PermissionTrieCache — process-singleton, keyed on (ClusterId, GenerationId).
  Install(trie) adds a generation + promotes to "current" when the id is
  highest-known (handles out-of-order installs gracefully). Prior generations
  retained so an in-flight request against a prior trie still succeeds; GC
  via Prune(cluster, keepLatest).
- UserAuthorizationState — per-session cache of resolved LDAP groups +
  AuthGenerationId + MembershipVersion + MembershipResolvedUtc. Bounded by
  MembershipFreshnessInterval (default 15 min per decision #151) +
  AuthCacheMaxStaleness (default 5 min per decision #152).
- TriePermissionEvaluator — default IPermissionEvaluator. Fails closed on
  stale sessions (IsStale check short-circuits to NotGranted), on cross-
  cluster requests, on empty trie cache. Maps OpcUaOperation → NodePermissions
  via MapOperationToPermission (total — every enum value has a mapping; tested).

Tests (27 new, all pass):
- PermissionTrieTests (7): cluster-level grant cascades to every tag;
  equipment-level grant doesn't leak to sibling equipment; multi-group union
  ORs flags; no-matching-group returns empty; Galaxy folder-segment grant
  doesn't leak to sibling folder; cross-cluster rows don't land in this
  cluster's trie; build is idempotent (B.6 invariants).
- TriePermissionEvaluatorTests (8): allow when flag matches; NotGranted when
  no matching group; NotGranted when flags insufficient; HistoryRead requires
  its own bit (decision-level requirement); cross-cluster session denied;
  stale session fails closed; no cached trie denied; MapOperationToPermission
  is total across every OpcUaOperation.
- PermissionTrieCacheTests (8): empty cache returns null; install-then-get
  round-trips; new generation becomes current; out-of-order install doesn't
  downgrade current; invalidate drops one cluster; prune retains most recent;
  prune no-op when fewer than keep; cluster isolation.
- UserAuthorizationStateTests (4): fresh is not stale; IsStale after 5 min
  default; NeedsRefresh true between freshness + staleness windows.

Full solution dotnet test: 1078 passing (baseline 906, Phase 6.1 = 1042,
Phase 6.2 Stream A = +9, Stream B = +27 = 1078). Pre-existing Client.CLI
Subscribe flake unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dohertj2 merged commit f74e141e64 into v2 2026-04-19 09:29:53 -04:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#85