diff --git a/docs/plans/2026-07-23-mesh-phase4-cut-driver-configdb.md b/docs/plans/2026-07-23-mesh-phase4-cut-driver-configdb.md index 01c79d57..7c34c9db 100644 --- a/docs/plans/2026-07-23-mesh-phase4-cut-driver-configdb.md +++ b/docs/plans/2026-07-23-mesh-phase4-cut-driver-configdb.md @@ -129,6 +129,49 @@ node: "driver-only node — no ConfigDb registered; config via ConfigSource:Mode --- +## Task 1b: driver-only `ILdapGroupRoleMappingService` — empty (appsettings-only group→role) — DISCOVERED during Task 1 + +**Classification:** small +**Estimated implement time:** ~4 min +**Parallelizable with:** Task 5 + +**Files:** +- Create: `src/Server/ZB.MOM.WW.OtOpcUa.Security/Ldap/NullLdapGroupRoleMappingService.cs` (or a Host-side + driver-only impl — place it beside `OtOpcUaGroupRoleMapper`) +- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs:317-327` (the `hasDriver` auth block — register the + empty service with `TryAddScoped` so a fused node keeps the real EF one; fix the now-false comment at + 322-323) +- Test: `tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests/` (or wherever `OtOpcUaGroupRoleMapper` is tested) + +**Context — a 6th ConfigDb consumer the §6.1 audit missed.** `OtOpcUaGroupRoleMapper` (the OPC UA +data-path group→role mapper, `IGroupRoleMapper`, registered at `Program.cs:327`) merges the +appsettings `Security:Ldap:GroupToRole` baseline (always available) with **system-wide DB grants** from +`ILdapGroupRoleMappingService` — which is registered **only** inside `AddOtOpcUaConfigDb`. Task 1 gated +that on `hasAdmin`, so a driver-only node now cannot resolve `IGroupRoleMapper` at OPC UA login +(lazy scoped resolve — `Build()` does not surface it; the auth path does). `LdapGroupRoleMapping` rows are +**not** in the deployment artifact (`ConfigComposer` never composes them), so driver nodes never received +DB grants via config in the first place. The honest fix: a driver-only `ILdapGroupRoleMappingService` whose +`GetByGroupsAsync` returns empty (CRUD methods throw `NotSupportedException` — never called with no AdminUI), +registered via `TryAddScoped` in the `hasDriver` block so the fused node's real EF impl still wins. +**Behavior reduction to document (Task 8):** DB-backed LDAP group→role grants (the AdminUI RoleGrants page) +apply only where ConfigDb is present (admin/fused nodes); driver-only site nodes map groups→roles from +`Security:Ldap:GroupToRole` appsettings only. + +**Step 1: Write the failing test** — resolve `IGroupRoleMapper` in a driver-only service graph and +call `MapAsync(["some-group"])` → returns the appsettings baseline with no throw (today it throws: +`ILdapGroupRoleMappingService` unresolvable). + +**Step 2:** Run — fails. + +**Step 3: Implement** the empty service + `TryAddScoped` registration in the `hasDriver` block; correct the +`Program.cs:322-323` comment to state the driver-only node uses the empty impl and why. + +**Step 4:** Run tests + build. + +**Step 5: Commit** — `fix(mesh-phase4): driver-only nodes map LDAP groups from appsettings (no ConfigDb grants)`. + +--- + ## Task 2: `DbHealthProbeActor` — do not spawn on driver-only; `DbReachable=true` constant **Classification:** high-risk @@ -346,7 +389,12 @@ that the driver-only node never serves login (it hosts no AdminUI — see the to **Step 1: Write the failing test** — a "driver-only DI graph resolves nothing ConfigDb-bound" test: build the `hasDriver && !hasAdmin` service provider and assert `GetService>()` is null AND that spawning the runtime actors -does not throw. +does not throw. **Also pin the Task-1b fused-node ordering** (Task 1b review gap): build the +`hasAdmin && hasDriver` graph through the same `AddOtOpcUaConfigDb` + `hasDriver` TryAdd sequence and assert +`GetService()` resolves the **real** `LdapGroupRoleMappingService`, not the +`Null…` — so a later reorder of the role blocks (or switching `AddOtOpcUaConfigDb` to a TryAdd) fails a +red test instead of silently stripping DB-backed role grants from central. Also resolve +`IGroupRoleMapper` in a driver-only scope to catch the lazy auth-path break Task 1b fixed. **Step 2:** Run — fix whatever it catches. diff --git a/docs/plans/2026-07-23-mesh-phase4-cut-driver-configdb.md.tasks.json b/docs/plans/2026-07-23-mesh-phase4-cut-driver-configdb.md.tasks.json index 9ea4a055..32d94fe7 100644 --- a/docs/plans/2026-07-23-mesh-phase4-cut-driver-configdb.md.tasks.json +++ b/docs/plans/2026-07-23-mesh-phase4-cut-driver-configdb.md.tasks.json @@ -1,18 +1,114 @@ { "planPath": "docs/plans/2026-07-23-mesh-phase4-cut-driver-configdb.md", "tasks": [ - {"id": 0, "subject": "Task 0: ConfigSourceOptionsValidator — driver-only ⇒ FetchAndCache", "status": "pending"}, - {"id": 1, "subject": "Task 1: Program.cs — gate ConfigDb registration + health check on hasAdmin", "status": "pending"}, - {"id": 2, "subject": "Task 2: DbHealthProbeActor — no spawn on driver-only; DbReachable=true constant", "status": "pending", "blockedBy": [1]}, - {"id": 3, "subject": "Task 3: OpcUaPublishActor — nullable factory; rebuild always carries bytes", "status": "pending", "blockedBy": [2]}, - {"id": 4, "subject": "Task 4: DriverHostActor — nullable factory; drop redundant SQL ack-writes", "status": "pending", "blockedBy": [1]}, - {"id": 5, "subject": "Task 5: alarm_condition_state LocalDb table + LocalDbAlarmConditionStateStore", "status": "pending"}, - {"id": 6, "subject": "Task 6: Wire LocalDb condition store into DriverHostActor for driver-role", "status": "pending", "blockedBy": [4, 5]}, - {"id": 7, "subject": "Task 7: Registration cleanup + full-graph driver-only DI sweep", "status": "pending", "blockedBy": [2, 3, 4, 6]}, - {"id": 8, "subject": "Task 8: Docs — Redundancy.md ServiceLevel + interop + Configuration.md + CLAUDE.md", "status": "pending", "blockedBy": [7]}, - {"id": 9, "subject": "Task 9: Drop dead ConfigDb ScriptedAlarmState table + retire EfAlarmConditionStateStore", "status": "pending", "blockedBy": [6]}, - {"id": 10, "subject": "Task 10: Rig — driver-only site nodes lose ConfigDb string", "status": "pending", "blockedBy": [7]}, - {"id": 11, "subject": "Task 11: Live gate", "status": "pending", "blockedBy": [8, 9, 10]} + { + "id": 0, + "subject": "Task 0: ConfigSourceOptionsValidator \u2014 driver-only \u21d2 FetchAndCache", + "status": "completed", + "commit": "d630a7e", + "result": "13/13 tests; APPROVED by code review" + }, + { + "id": 1, + "subject": "Task 1: Program.cs \u2014 gate ConfigDb registration + health check on hasAdmin", + "status": "completed", + "commit": "a41582ea", + "result": "8/8 tests; surfaced Task 1b (IGroupRoleMapper ConfigDb dep)" + }, + { + "id": "1b", + "subject": "Task 1b: driver-only ILdapGroupRoleMappingService \u2014 empty (appsettings-only)", + "status": "completed", + "blockedBy": [ + 1 + ], + "commit": "833e45db", + "result": "9/9 tests; APPROVED (Task 7 to pin fused-node ordering)" + }, + { + "id": 2, + "subject": "Task 2: DbHealthProbeActor \u2014 no spawn on driver-only; DbReachable=true constant", + "status": "pending", + "blockedBy": [ + 1 + ] + }, + { + "id": 3, + "subject": "Task 3: OpcUaPublishActor \u2014 nullable factory; rebuild always carries bytes", + "status": "pending", + "blockedBy": [ + 2 + ] + }, + { + "id": 4, + "subject": "Task 4: DriverHostActor \u2014 nullable factory; drop redundant SQL ack-writes", + "status": "pending", + "blockedBy": [ + 1 + ] + }, + { + "id": 5, + "subject": "Task 5: alarm_condition_state LocalDb table + LocalDbAlarmConditionStateStore", + "status": "pending" + }, + { + "id": 6, + "subject": "Task 6: Wire LocalDb condition store into DriverHostActor for driver-role", + "status": "pending", + "blockedBy": [ + 4, + 5 + ] + }, + { + "id": 7, + "subject": "Task 7: Registration cleanup + full-graph driver-only DI sweep", + "status": "pending", + "blockedBy": [ + "1b", + 2, + 3, + 4, + 6 + ] + }, + { + "id": 8, + "subject": "Task 8: Docs \u2014 Redundancy.md ServiceLevel + interop + Configuration.md + CLAUDE.md", + "status": "pending", + "blockedBy": [ + 7 + ] + }, + { + "id": 9, + "subject": "Task 9: Drop dead ConfigDb ScriptedAlarmState table + retire EfAlarmConditionStateStore", + "status": "pending", + "blockedBy": [ + 6 + ] + }, + { + "id": 10, + "subject": "Task 10: Rig \u2014 driver-only site nodes lose ConfigDb string", + "status": "pending", + "blockedBy": [ + 7 + ] + }, + { + "id": 11, + "subject": "Task 11: Live gate", + "status": "pending", + "blockedBy": [ + 8, + 9, + 10 + ] + } ], "lastUpdated": "2026-07-23" }