Phase 6.2 Stream C follow-up - wire AuthorizationGate into DriverNodeManager Read/Write/HistoryRead #94

Merged
dohertj2 merged 1 commits from phase-6-2-stream-c-dispatch-wiring-followup into v2 2026-04-19 11:04:22 -04:00
Owner

Closes release blocker #1 from docs/v2/v2-release-readiness.md — the Phase 6.2 evaluator is now called by real dispatch paths instead of being dead code.

Summary

  • NodeScopeResolver — Phase 1 flat scope (ClusterId + TagId). Cluster-level ACLs cascade per decision #129; finer-grained resolution is Stream C.12 follow-up.
  • WriteAuthzPolicy.ToOpcUaOperation(classification) — maps driver-reported SecurityClassification to OpcUaOperation Write{Operate/Tune/Configure}.
  • DriverNodeManager:
    • Ctor gains optional AuthorizationGate + NodeScopeResolver; both null preserves pre-Phase-6.2 dispatch for existing tests.
    • OnReadValue calls gate before invoker; denied reads return BadUserAccessDenied without hitting the driver.
    • OnWriteValue adds additive gate check alongside existing WriteAuthzPolicy classification-vs-roles check.
    • All four HistoryRead paths (Raw/Processed/AtTime/Events) gate per-node; new WriteAccessDenied helper surfaces BadUserAccessDenied in the result slot.
    • Event-history path tolerates fullRef == null (notifier / driver-root reads are cluster-wide; scope shape for those is a separate follow-up).
  • OtOpcUaServer + OpcUaApplicationHost: thread gate + resolver as optional params (same pattern as DriverResiliencePipelineBuilder).
  • Production activation: construct OpcUaApplicationHost with an AuthorizationGate(StrictMode: true) once ACL data is seeded. Lax default during rollout keeps older deployments working.

Test plan

  • 5 new NodeScopeResolver tests: round-trip fields, Phase 1 null-ness of finer path, empty-string + empty-cluster validation, stateless.
  • Full solution dotnet test: 1164 passing (was 1159, +5).
  • Existing AuthorizationGate strict/lax tests (PR #86) continue to pass.
  • Existing OpcUaApplicationHost / HealthEndpointsHost / driver integration tests pass because gate defaults to null → no enforcement.

🤖 Generated with Claude Code

Closes release blocker #1 from `docs/v2/v2-release-readiness.md` — the Phase 6.2 evaluator is now called by real dispatch paths instead of being dead code. ## Summary - `NodeScopeResolver` — Phase 1 flat scope (ClusterId + TagId). Cluster-level ACLs cascade per decision #129; finer-grained resolution is Stream C.12 follow-up. - `WriteAuthzPolicy.ToOpcUaOperation(classification)` — maps driver-reported SecurityClassification to OpcUaOperation Write{Operate/Tune/Configure}. - `DriverNodeManager`: - Ctor gains optional `AuthorizationGate` + `NodeScopeResolver`; both null preserves pre-Phase-6.2 dispatch for existing tests. - `OnReadValue` calls gate before invoker; denied reads return BadUserAccessDenied without hitting the driver. - `OnWriteValue` adds additive gate check alongside existing WriteAuthzPolicy classification-vs-roles check. - All four HistoryRead paths (Raw/Processed/AtTime/Events) gate per-node; new `WriteAccessDenied` helper surfaces BadUserAccessDenied in the result slot. - Event-history path tolerates `fullRef == null` (notifier / driver-root reads are cluster-wide; scope shape for those is a separate follow-up). - `OtOpcUaServer` + `OpcUaApplicationHost`: thread gate + resolver as optional params (same pattern as DriverResiliencePipelineBuilder). - Production activation: construct `OpcUaApplicationHost` with an `AuthorizationGate(StrictMode: true)` once ACL data is seeded. Lax default during rollout keeps older deployments working. ## Test plan - [x] 5 new NodeScopeResolver tests: round-trip fields, Phase 1 null-ness of finer path, empty-string + empty-cluster validation, stateless. - [x] Full solution `dotnet test`: 1164 passing (was 1159, +5). - [x] Existing AuthorizationGate strict/lax tests (PR #86) continue to pass. - [x] Existing OpcUaApplicationHost / HealthEndpointsHost / driver integration tests pass because gate defaults to null → no enforcement. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
dohertj2 added 1 commit 2026-04-19 11:04:11 -04:00
Closes the Phase 6.2 security gap the v2 release-readiness dashboard flagged:
the evaluator + trie + gate shipped as code in PRs #84-88 but no dispatch
path called them. This PR threads the gate end-to-end from
OpcUaApplicationHost → OtOpcUaServer → DriverNodeManager and calls it on
every Read / Write / 4 HistoryRead paths.

Server.Security additions:
- NodeScopeResolver — maps driver fullRef → Core.Authorization NodeScope.
  Phase 1 shape: populates ClusterId + TagId; leaves NamespaceId / UnsArea /
  UnsLine / Equipment null. The cluster-level ACL cascade covers this
  configuration (decision #129 additive grants). Finer-grained scope
  resolution (joining against the live Configuration DB for UnsArea / UnsLine
  path) lands as Stream C.12 follow-up.
- WriteAuthzPolicy.ToOpcUaOperation — maps SecurityClassification → the
  OpcUaOperation the gate evaluator consults (Operate/SecuredWrite →
  WriteOperate; Tune → WriteTune; Configure/VerifiedWrite → WriteConfigure).

DriverNodeManager wiring:
- Ctor gains optional AuthorizationGate + NodeScopeResolver; both null means
  the pre-Phase-6.2 dispatch runs unchanged (backwards-compat for every
  integration test that constructs DriverNodeManager directly).
- OnReadValue: ahead of the invoker call, builds NodeScope + calls
  gate.IsAllowed(identity, Read, scope). Denied reads return
  BadUserAccessDenied without hitting the driver.
- OnWriteValue: preserves the existing WriteAuthzPolicy check (classification
  vs session roles) + adds an additive gate check using
  WriteAuthzPolicy.ToOpcUaOperation(classification) to pick the right
  WriteOperate/Tune/Configure surface. Lax mode falls through for identities
  without LDAP groups.
- Four HistoryRead paths (Raw / Processed / AtTime / Events): gate check
  runs per-node before the invoker. Events path tolerates fullRef=null
  (event-history queries can target a notifier / driver-root; those are
  cluster-wide reads that need a different scope shape — deferred).
- New WriteAccessDenied helper surfaces BadUserAccessDenied in the
  OpcHistoryReadResult slot + errors list, matching the shape of the
  existing WriteUnsupported / WriteInternalError helpers.

OtOpcUaServer + OpcUaApplicationHost: gate + resolver thread through as
optional constructor parameters (same pattern as DriverResiliencePipelineBuilder
in Phase 6.1). Null defaults keep the existing 3 OpcUaApplicationHost
integration tests constructing without them unchanged.

Tests (5 new in NodeScopeResolverTests):
- Resolve populates ClusterId + TagId + Equipment Kind.
- Resolve leaves finer path null per Phase 1 shape (doc'd as follow-up).
- Empty fullReference throws.
- Empty clusterId throws at ctor.
- Resolver is stateless across calls.

The existing 9 AuthorizationGate tests (shipped in PR #86) continue to
cover the gate's allow/deny semantics under strict + lax mode.

Full solution dotnet test: 1164 passing (was 1159, +5). Pre-existing
Client.CLI Subscribe flake unchanged. Existing OpcUaApplicationHost +
HealthEndpointsHost + driver integration tests continue to pass because the
gate defaults to null → no enforcement, and the lax-mode fallback returns
true for identities without LDAP groups (the anonymous test path).

Production deployments flip the gate on by constructing it via
OpcUaApplicationHost's new authzGate parameter + setting
`Authorization:StrictMode = true` once ACL data is populated. Flipping the
switch post-seed turns the evaluator + trie from scaffolded code into
actual enforcement.

This closes release blocker #1 listed in docs/v2/v2-release-readiness.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dohertj2 merged commit b912969805 into v2 2026-04-19 11:04:22 -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#94