Phase 3 PR 26 — server-layer write authorization by role #25

Merged
dohertj2 merged 1 commits from phase-3-pr26-server-write-authz into v2 2026-04-18 13:04:36 -04:00
Owner

ACL enforcement moves to the OPC UA server per user's architectural directive. WriteAuthzPolicy maps SecurityClassification → required role (FreeAccess = no role, Operate/SecuredWrite = WriteOperate, Tune = WriteTune, VerifiedWrite/Configure = WriteConfigure, ViewOnly = deny). DriverNodeManager.OnWriteValue caches classification per variable and checks session roles (via new IRoleBearer) before calling IWritable.WriteAsync. Role requirements don't cascade — escalation is an explicit LDAP group assignment. 17 new unit tests + lmx-followups doc updated. 38/38 Server Unit + 2/2 Server Integration pass.

ACL enforcement moves to the OPC UA server per user's architectural directive. WriteAuthzPolicy maps SecurityClassification → required role (FreeAccess = no role, Operate/SecuredWrite = WriteOperate, Tune = WriteTune, VerifiedWrite/Configure = WriteConfigure, ViewOnly = deny). DriverNodeManager.OnWriteValue caches classification per variable and checks session roles (via new IRoleBearer) before calling IWritable.WriteAsync. Role requirements don't cascade — escalation is an explicit LDAP group assignment. 17 new unit tests + lmx-followups doc updated. 38/38 Server Unit + 2/2 Server Integration pass.
dohertj2 added 1 commit 2026-04-18 13:04:32 -04:00
Phase 3 PR 26 — server-layer write authorization gating by role. Per the user's ACL-at-server-layer directive (saved as feedback_acl_at_server_layer.md in memory), write authorization is enforced in DriverNodeManager.OnWriteValue and never delegated to the driver or to driver-specific auth (the v1 Galaxy-provided security path is explicitly not part of v2 — drivers report SecurityClassification as discovery metadata only). New WriteAuthzPolicy static class in Server/Security/ maps SecurityClassification → required role per the table documented in docs/Configuration.md: FreeAccess = no role required (anonymous sessions can write), Operate + SecuredWrite = WriteOperate, Tune = WriteTune, VerifiedWrite + Configure = WriteConfigure, ViewOnly = deny regardless of roles. Role matching is case-insensitive and role requirements do NOT cascade — a session with WriteConfigure can write Configure attributes but needs WriteOperate separately to write Operate attributes; this is deliberate so escalation is an explicit LDAP group assignment, not a hierarchy the policy silently grants. DriverNodeManager gains a _securityByFullRef Dictionary populated during Variable() registration (parallel to the existing _variablesByFullRef) so OnWriteValue can look up the classification in O(1) on the hot path. OnWriteValue casts the session's context.UserIdentity to the new IRoleBearer interface (implemented by OtOpcUaServer.RoleBasedIdentity from PR 19) — empty Roles collection when the session is anonymous; the same WriteAuthzPolicy.IsAllowed check then either short-circuits true (FreeAccess), false (ViewOnly), or walks the roles list looking for the required one. On deny, OnWriteValue logs 'Write denied for {FullRef}: classification=X userRoles=[...]' at Information level (readable trail for operator complaints) and returns BadUserAccessDenied without touching IWritable.WriteAsync — drivers never see a request we'd have refused. IRoleBearer kept as a minimal server-side interface rather than reusing some abstraction from Core.Abstractions because the concept is OPC-UA-session-scoped and doesn't generalize (the driver side has no notion of a user session). Tests — WriteAuthzPolicyTests (17 new cases): FreeAccess allows write with empty role set + arbitrary roles; ViewOnly denies write even with every role; Operate requires WriteOperate; role match is case-insensitive; Operate denies empty role set + wrong role; SecuredWrite shares Operate's requirement; Tune requires WriteTune; Tune denies WriteOperate-only (asserts roles don't cascade — this is the test that catches a future regression where someone 'helpfully' adds a role-escalation table); Configure requires WriteConfigure; VerifiedWrite shares Configure's requirement; multi-role session allowed when any role matches; unrelated roles denied; RequiredRole theory covering all 5 classified-and-mapped rows + null for FreeAccess/ViewOnly special cases. lmx-followups.md follow-up #2 marked DONE with a back-reference to this PR and the memory note. Full Server.Tests Unit suite: 38 pass / 0 fail (17 new WriteAuthz + 14 SecurityConfiguration from PR 19 + 2 NodeBootstrap + 5 others). Server.Tests Integration (Category=Integration) 2 pass — existing PR 17 anonymous-endpoint smoke tests stay green since the read path doesn't hit OnWriteValue. 6b04a85f86
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dohertj2 merged commit 4058b88784 into v2 2026-04-18 13:04:36 -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#25