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>