fix(secrets): SqlServer/Akka extensions take contentRootPath; last explicit root wins (scadaproj#31)

The SQL-Server extensions called the 3-arg AddZbSecrets, and the store-path
validator captured the content root of whichever call registered it first.
In hub mode no registration order applied the pre-host under-content-root
guard: extension-first dropped the app's 4-arg root, core-first lost the
replicator (#30). That is the 0.6.0 store-path hole, reopened.

- AddZbSecretsSqlServerStore / AddZbSecretsSqlServerReplication /
  AddZbSecretsAkkaReplication gain a trailing optional contentRootPath,
  forwarded to the 4-arg AddZbSecrets
- the root now lives in one SecretsContentRoot instance holder per service
  collection; the last explicit root wins, null never erases one; the
  IHostEnvironment fallback is unchanged
- tests: ContentRootTests (hub mode, three compositions, artifact asserted
  before the throw, positive control), last-explicit-wins theory in
  SecretsStorePathRulesTests, Akka forwarding test
- code-reviews: Secrets-002 Resolved; README regenerated

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 14:50:07 -05:00
parent 015c556ced
commit c3b07d6766
10 changed files with 272 additions and 18 deletions
+5 -6
View File
@@ -40,7 +40,7 @@ code-reviews/
## Summary
10 of 10 libraries reviewed. 29 pending findings across all libraries.
10 of 10 libraries reviewed. 28 pending findings across all libraries.
Coverage: Secrets, LocalDb, GalaxyRepository and Overview first reviewed, and Auth, Health,
Theme, Audit, Configuration and Telemetry re-reviewed, on 2026-11-09 (#15). Each finding also
@@ -50,9 +50,9 @@ carries a P1/P2/P3 priority (P1 = Critical/High, P2 = Medium, P3 = Low).
|----------|---------------|
| Critical | 0 |
| High | 0 |
| Medium | 10 |
| Medium | 9 |
| Low | 19 |
| **Total** | **29** |
| **Total** | **28** |
## Library Status
@@ -65,7 +65,7 @@ carries a P1/P2/P3 priority (P1 = Critical/High, P2 = Medium, P3 = Low).
| [Health](Health/findings.md) | 2026-11-09 | `91fb2b4` | 0/0/0/0 | 0 | 7 |
| [LocalDb](LocalDb/findings.md) | 2026-11-09 | `91fb2b4` | 0/0/4/3 | 7 | 7 |
| [Overview](Overview/findings.md) | 2026-11-09 | `91fb2b4` | 0/0/0/0 | 0 | 5 |
| [Secrets](Secrets/findings.md) | 2026-11-09 | `91fb2b4` | 0/0/5/13 | 18 | 19 |
| [Secrets](Secrets/findings.md) | 2026-11-09 | `91fb2b4` | 0/0/4/13 | 17 | 19 |
| [Telemetry](Telemetry/findings.md) | 2026-11-09 | `91fb2b4` | 0/0/0/0 | 0 | 11 |
| [Theme](Theme/findings.md) | 2026-11-09 | `91fb2b4` | 0/0/0/0 | 0 | 8 |
@@ -84,7 +84,7 @@ _None open._
_None open._
### Medium (10)
### Medium (9)
| ID | Library | Title |
|----|---------|-------|
@@ -93,7 +93,6 @@ _None open._
| LocalDb-002 | [LocalDb](LocalDb/findings.md) | Snapshot streams tables by name and rows by `pk_json` text, so FK children land before parents and are dead-lettered for good |
| LocalDb-003 | [LocalDb](LocalDb/findings.md) | `INSERT OR REPLACE` apply silently deletes a different row on a secondary `UNIQUE` conflict, so the pair diverges |
| LocalDb-004 | [LocalDb](LocalDb/findings.md) | `FailClosedOnDrift` is bypassed on the snapshot path, so a future-stamped snapshot row moves the local HLC forward permanently |
| Secrets-002 | [Secrets](Secrets/findings.md) | The SQL-Server extensions cannot pass the content root, so the pre-host store-path hole reopens in hub mode |
| Secrets-003 | [Secrets](Secrets/findings.md) | `${secret:}` expansion is undone by configuration reload |
| Secrets-010 | [Secrets](Secrets/findings.md) | Akka replication actor is created only when `ISecretStore` is first resolved; a node that never resolves it never replicates |
| Secrets-013 | [Secrets](Secrets/findings.md) | UI add/rotate/delete: if the audit write throws after the store write succeeded, the change is reported and audited as a failure and the resolver cache is not evicted |
+3 -3
View File
@@ -10,7 +10,7 @@
| Last reviewed | 2026-11-09 |
| Reviewer | Claude (scadaproj#15) |
| Commit reviewed | `91fb2b4` |
| Open findings | 18 |
| Open findings | 17 |
## Summary
@@ -98,7 +98,7 @@ Resolved 2026-11-09 in the commit tagged `(scadaproj#30)` (Secrets `0.6.6`, unpu
| Severity | Medium |
| Priority | P2 |
| Category | Security & secret handling |
| Status | Open |
| Status | Resolved |
| Location | `ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Replicator.SqlServer/DependencyInjection/SqlServerSecretsServiceCollectionExtensions.cs:94` (with `ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets/DependencyInjection/SecretsServiceCollectionExtensions.cs:76`) |
**Description**
@@ -119,7 +119,7 @@ Add a `string? contentRootPath` parameter to both SQL-Server extensions and forw
**Resolution**
_Unresolved._
Resolved 2026-11-09 in the commit tagged `(scadaproj#31)` (Secrets `0.6.6`, unpublished). Both recommendations done: (1) `AddZbSecretsSqlServerStore`, `AddZbSecretsSqlServerReplication` and `AddZbSecretsAkkaReplication` gained a trailing optional `string? contentRootPath = null`, forwarded to the 4-arg `AddZbSecrets`; (2) the content root moved out of the first-registering call's closure into one shared `SecretsContentRoot` instance holder in the service collection, which every `AddZbSecrets` call may set — the last explicit root wins and a `null` never erases an earlier one; the validator factory still falls back to `IHostEnvironment` only when no explicit root was given. Regression tests: `Replicator.SqlServer.Tests/DependencyInjection/ContentRootTests` (hub mode refuses an in-root store via the extension argument, extension-then-core and core-then-extension, artifact asserted before the throw, with an outside-root positive control; shared-store mode forwards too), `SecretsStorePathRulesTests.The_last_explicit_content_root_wins_whichever_call_registered_the_validator`, and `AddZbSecretsAkkaReplicationTests.The_content_root_argument_reaches_the_store_path_guard`. Without the holder change the extension-then-core and 3-arg-first cases create the store and do not throw.
### Secrets-003 — `${secret:}` expansion is undone by configuration reload