fb6dd3478d
Closes task #133 — the "authz gate is inert in production" blocker surfaced during task #123. Before this commit, every ACL check on the six dispatch surfaces (Read, Write, HistoryRead, Browse, CreateMonitoredItems, Call) short-circuited to allow because Program.cs constructed OpcUaApplicationHost without passing authzGate or scopeResolver. New pieces: - `AuthorizationOptions` — bound to `Node:Authorization` in appsettings.json. `Enabled` (default false) is the master switch; `StrictMode` (default false) controls the anonymous / no-LDAP-groups fallback behaviour. - `AuthorizationBootstrap` — singleton service that loads `NodeAcl` rows for the published generation, builds a `PermissionTrieCache` + `AuthorizationGate`, merges every registered driver's `EquipmentNamespaceContent` through `ScopePathIndexBuilder` into one full-path `NodeScopeResolver`. Returns `(null, null)` when disabled or when no generation is Published yet. - `DriverEquipmentContentRegistry.Snapshot()` — new method returning a defensive copy of the driver → content map so the bootstrap can iterate without holding the lock. - `OpcUaApplicationHost.SetAuthorization(gate, resolver)` — late-bind method matching the existing `SetPhase7Sources` pattern. Must run before `StartAsync`; rejects post-start rebinding with InvalidOperationException. - `OpcUaServerService.ExecuteAsync` calls `AuthorizationBootstrap.BuildAsync` after `PopulateEquipmentContentAsync` and before `applicationHost.StartAsync`, in the same window that `SetPhase7Sources` runs. Behaviour change - Default (Enabled=false): no behaviour change — the gate stays null, all six dispatch surfaces run unchanged. Safe for any existing deployment on upgrade. - Enabled=true with StrictMode=false: identities carrying LDAP groups are evaluated against the trie; anonymous / no-groups identities pass through (v1 legacy-client compatibility). - Enabled=true with StrictMode=true: everything evaluates. Anonymous or no-groups identities are denied. Follow-up not covered here: rebind the gate+resolver on generation refresh (the `GenerationRefreshHostedService` that shipped earlier in this session). Today the gate only reflects the bootstrap generation — operators publishing new ACL changes need a process restart to see them. Matches the current driver-hot-reload limitation and is tracked in the existing 6.3 follow-up bullet. Docs: v2-release-readiness.md Phase 6.2 Stream C.12 bullet flipped to Closed with operator-facing config pointer (`Node:Authorization:Enabled`). All 283/283 Server.Tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>