Anonymous OPC UA sessions had no roles (`UserIdentity()`), so
`WriteAuthzPolicy.IsAllowed(SecurityClassification.Operate, [])`
rejected every write with `BadUserAccessDenied`. The reverse-write
stage of the Modbus e2e script surfaced this: stages 1-3 + 5 pass
forward-direction, stage 4 (OPC UA client → server → driver → PLC)
blew up with `0x801F0000` even with the factory + seed perfectly
wired.
Adds a single config knob:
"OpcUaServer": {
"AnonymousRoles": ["WriteOperate"]
}
Default empty preserves the pre-existing production-safe behaviour
(anonymous reads FreeAccess tags, rejected on everything else). When
non-empty, `OtOpcUaServer.OnImpersonateUser` wraps the anonymous token
in a `RoleBasedIdentity("(anonymous)", "Anonymous", AnonymousRoles)`
so the server-layer write guard sees the configured roles.
Wire-through:
- OpcUaServerOptions.AnonymousRoles (new)
- OpcUaApplicationHost passes it to OtOpcUaServer ctor
- OtOpcUaServer new anonymousRoles ctor param + OnImpersonateUser
branch
- Program.cs reads `OpcUaServer:AnonymousRoles` section from config
Env override syntax: `OpcUaServer__AnonymousRoles__0=WriteOperate`.
## Verified live
Booted server against `seed-modbus-smoke.sql` with
`OpcUaServer__AnonymousRoles__0=WriteOperate` + pymodbus fixture →
`test-modbus.ps1 -BridgeNodeId "ns=2;s=HR200"`:
=== Modbus e2e summary: 5/5 passed ===
[PASS] Probe
[PASS] Driver loopback
[PASS] Server bridge (driver → server → client)
[PASS] OPC UA write bridge (client → server → driver)
[PASS] Subscribe sees change
All five stages green end-to-end. Issue #219 closed by this PR; the
Modbus-seed update to set AnonymousRoles lives in the follow-up #220
live-boot PR (same AnonymousRoles value applies to every driver since
the classification is a driver-constant, not per-tag).
Full-solution build: 0 errors, only pre-existing xUnit1051 warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>