fix(management-service): resolve ManagementService-005,008,010,011 — supervision strategy, configured command timeout, remove stale ResolveRoles path; ManagementService-012 deferred

This commit is contained in:
Joseph Doherty
2026-05-16 22:24:03 -04:00
parent 858fe24add
commit dab0056d1b
6 changed files with 200 additions and 32 deletions

View File

@@ -8,7 +8,7 @@
| Last reviewed | 2026-05-16 |
| Reviewer | claude-agent |
| Commit reviewed | `9c60592` |
| Open findings | 5 |
| Open findings | 0 (1 Deferred — see ManagementService-012) |
## Summary
@@ -211,7 +211,7 @@ mapping tests confirm behaviour is preserved.
|--|--|
| Severity | Low |
| Category | Akka.NET conventions |
| Status | Open |
| Status | Resolved |
| Location | `src/ScadaLink.ManagementService/ManagementActor.cs:33` |
**Description**
@@ -230,7 +230,15 @@ Resume-based strategy, consistent with other central coordinator actors.
**Resolution**
_Unresolved._
Resolved 2026-05-16 (commit pending). Confirmed: `ManagementActor` declared no
`SupervisorStrategy`. Added a `public static SupervisorStrategy CreateSupervisorStrategy()`
factory returning an unbounded `OneForOneStrategy` with a `Directive.Resume` decider, and a
`protected override SupervisorStrategy()` that delegates to it — matching the Resume-based
convention of `CentralCommunicationActor`/`SiteCommunicationActor`. The actor spawns no
children today, so this is a forward-looking correctness fix. Regression tests:
`CreateSupervisorStrategy_ReturnsOneForOneStrategy`,
`CreateSupervisorStrategy_ResumesOnArbitraryException`,
`CreateSupervisorStrategy_ResumesIndefinitely` (new `ManagementActorSupervisionTests.cs`).
### ManagementService-006 — JsonDocument instances never disposed in the HTTP endpoint
@@ -312,7 +320,7 @@ Regression tests: `SerializeResult_WithCyclicGraph_DoesNotThrow`,
|--|--|
| Severity | Low |
| Category | Correctness & logic bugs |
| Status | Open |
| Status | Resolved |
| Location | `src/ScadaLink.ManagementService/ManagementActor.cs:285` |
**Description**
@@ -329,7 +337,15 @@ Resolve `RoleMapper` via `sp.GetRequiredService<RoleMapper>()` like every other
**Resolution**
_Unresolved._
Resolved 2026-05-16 (commit pending). Confirmed: `HandleResolveRoles` did
`new RoleMapper(sp.GetRequiredService<ISecurityRepository>())`, bypassing the
`AddScoped<RoleMapper>()` DI registration. The hand-built `RoleMapper` lived only inside
`HandleResolveRoles`, which is itself the dead-code dispatch path removed under finding 011
(the two-step ResolveRoles flow is retired). Resolving 011 by deleting the
`ResolveRolesCommand` dispatch case and `HandleResolveRoles` handler also removes the only
manually-constructed `RoleMapper` in the module, so the DI-bypass no longer exists. No
remaining `new RoleMapper` in `src/ScadaLink.ManagementService`. Regression covered by
`ResolveRolesCommand_IsNoLongerDispatched_ReturnsManagementError`.
### ManagementService-009 — Audit logging applied inconsistently across mutating handlers
@@ -385,7 +401,7 @@ covers the explicit-audit path.
|--|--|
| Severity | Low |
| Category | Design-document adherence |
| Status | Open |
| Status | Resolved |
| Location | `src/ScadaLink.ManagementService/ManagementServiceOptions.cs:5`; `src/ScadaLink.ManagementService/ManagementEndpoints.cs:16` |
**Description**
@@ -405,7 +421,15 @@ configuration cannot be set with no effect.
**Resolution**
_Unresolved._
Resolved 2026-05-16 (commit pending). Confirmed: `ManagementEndpoints` hard-coded
`AskTimeout = TimeSpan.FromSeconds(30)` and never read `ManagementServiceOptions.CommandTimeout`.
`HandleRequest` now resolves `IOptions<ManagementServiceOptions>` from `context.RequestServices`
and computes the Ask timeout via a new `ManagementEndpoints.ResolveAskTimeout` helper, which
returns the configured `CommandTimeout` when strictly positive and otherwise falls back to the
30s default (guarding against a misconfigured zero/negative value that would fail every call).
Regression tests: `ResolveAskTimeout_UsesConfiguredCommandTimeout`,
`ResolveAskTimeout_WithNullOptions_FallsBackToDefault`,
`ResolveAskTimeout_WithNonPositiveTimeout_FallsBackToDefault`.
### ManagementService-011 — ResolveRolesCommand dispatch path is stale dead code
@@ -413,7 +437,7 @@ _Unresolved._
|--|--|
| Severity | Low |
| Category | Correctness & logic bugs |
| Status | Open |
| Status | Resolved |
| Location | `src/ScadaLink.ManagementService/ManagementActor.cs:273`, `:283` |
**Description**
@@ -435,7 +459,19 @@ data unauthenticated is intended.
**Resolution**
_Unresolved._
Resolved 2026-05-16 (commit pending). Confirmed dead path: a repository-wide search found no
`ResolveRolesCommand` sender outside `ManagementActor` itself — the CLI and HTTP endpoint
perform LDAP auth + role resolution inline. Removed the `ResolveRolesCommand` dispatch case
and the `HandleResolveRoles` handler from `ManagementActor`; a stray ClusterClient sender now
falls through to the `NotSupportedException` default and gets a uniform `ManagementError`
(closing the unauthenticated role-mapping enumeration surface, since `GetRequiredRole`
returned null for it). A code comment at the former dispatch site documents the intentional
omission. Note: the `ResolveRolesCommand` *record* itself lives in
`src/ScadaLink.Commons/Messages/Management/SecurityCommands.cs` and was left in place — that
file is outside this module's permitted edit scope; deleting the orphan record should be done
as a Commons-module follow-up. With the handler removed it is now an inert,
registry-only type with no behaviour. Regression test:
`ResolveRolesCommand_IsNoLongerDispatched_ReturnsManagementError`.
### ManagementService-012 — ManagementEnvelope carries a loosely-typed object payload
@@ -443,7 +479,7 @@ _Unresolved._
|--|--|
| Severity | Low |
| Category | Akka.NET conventions |
| Status | Open |
| Status | Deferred |
| Location | `src/ScadaLink.Commons/Messages/Management/ManagementEnvelope.cs:7`; `src/ScadaLink.ManagementService/ManagementActor.cs:132` |
**Description**
@@ -463,7 +499,16 @@ flag unhandled cases, and keeps `ManagementCommandRegistry`'s reflection scan pr
**Resolution**
_Unresolved._
Deferred 2026-05-16. Finding verified as genuine: `ManagementEnvelope.Command` is typed
`object` and the recommended `IManagementCommand` marker-interface fix is sound. However, the
fix cannot be implemented within the `ManagementService` module: both `ManagementEnvelope` and
all ~50 `*Command` records live in `src/ScadaLink.Commons/Messages/Management/` (17 files),
which is outside this work item's permitted edit scope (`src/ScadaLink.ManagementService/**`,
its tests, and this findings file only). Adding the marker interface, retyping the envelope,
and having `ManagementCommandRegistry` constrain its reflection scan to `IManagementCommand`
implementers is a cohesive Commons-module change and must be done there — also so the Commons
message-contract additive-only evolution rules are respected. Deferred to a Commons-module
work item; no `ManagementService`-local change is appropriate.
### ManagementService-013 — No tests for site-scope enforcement, the HTTP endpoint, or DebugStreamHub