Task #219 — OpcUaServerOptions.AnonymousRoles (5/5 e2e stages pass) #221
Reference in New Issue
Block a user
Delete Branch "task-219-anonymous-roles"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #219.
Root cause
Anonymous OPC UA sessions landed as plain
UserIdentity()— no roles.WriteAuthzPolicy.IsAllowed(SecurityClassification.Operate, [])rejects the empty role set, soDriverNodeManager.OnWriteValuereturnedBadUserAccessDenied(0x801F0000) for every write against a tag the driver classified asOperate(i.e. every writable tag).Fix
Single config knob on
OpcUaServerOptions:Default is empty, preserving the production-safe behaviour (anonymous can read
FreeAccess, rejected onOperate/Tune/Configure). When non-empty,OtOpcUaServer.OnImpersonateUserwraps the anonymous token in aRoleBasedIdentity("(anonymous)", "Anonymous", AnonymousRoles)soDriverNodeManager's write guard sees the configured roles.Env override syntax:
OpcUaServer__AnonymousRoles__0=WriteOperate.Verified live (5/5 stages)
Booted server against
seed-modbus-smoke.sqlwithOpcUaServer__AnonymousRoles__0=WriteOperate+ pymodbus fixture →test-modbus.ps1 -BridgeNodeId "ns=2;s=HR200":Both directions + subscription delivery work through the server end-to-end. Stage 4 — the one that was failing — PASSes with the driver-side read confirming the OPC UA write propagated:
PLC-side value equals 20675on a reverse-write of20675.Files
src/.../Server/OpcUa/OpcUaServerOptions.cs— newAnonymousRolesproperty (IReadOnlyList, default [])src/.../Server/OpcUa/OtOpcUaServer.cs— newanonymousRolesctor param, used inOnImpersonateUsersrc/.../Server/OpcUa/OpcUaApplicationHost.cs— passes_options.AnonymousRolestoOtOpcUaServersrc/.../Server/Program.cs— readsOpcUaServer:AnonymousRolessection from configTest plan
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>