docs(mesh-phase4): add Task 1b (driver-only LDAP mapper) discovered during Task 1
Gating ConfigDb on hasAdmin exposed a 6th driver-side ConfigDb consumer the §6.1 audit missed: OtOpcUaGroupRoleMapper's ILdapGroupRoleMappingService dep. Task 7 gains a fused-node ordering pin (Task 1b review gap). Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
@@ -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<string>`, 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<string>` 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<string>` 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<IDbContextFactory<OtOpcUaConfigDbContext>>()` 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<ILdapGroupRoleMappingService>()` 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<string>` in a driver-only scope to catch the lazy auth-path break Task 1b fixed.
|
||||
|
||||
**Step 2:** Run — fix whatever it catches.
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user