Commit Graph

5 Commits

Author SHA1 Message Date
Joseph Doherty 6f1787abe1 fix(site-secrets): mirror heals same-revision local drift and isolates unreadable rows
Final review I1: a name already ledgered at the shipped revision is now read back every
pass; a missing, tombstoned or edited local row is healed through the mirror-owned re-stamp
path (bump, apply, read back, invalidate) before verify, instead of a local delete being
misreported as a KEK mismatch that blocked every later deploy. Drift logs Warning once per
occurrence. M4: row_json that cannot be parsed becomes a Failed entry for that name via the
new SiteStorageService.GetDeployedSecretsTolerantAsync; an unreadable removed row is still
honoured by name. M10: invalidator ctor doc corrected.
2026-09-28 21:08:34 -04:00
Joseph Doherty 52cd4fb096 feat(site): artifact apply writes deployed secrets and answers SecretsApplied 2026-09-28 19:29:55 -04:00
Joseph Doherty 838fb4177b fix(site): DeployedSecretsMirror — ledger-owned rows recover past clock skew; real-cipher recovery test
Fix round 2 (review task-9-rereview-1.md), both Minors:

1. Clock skew. ApplyAndConfirmAsync now decides the tombstone-vs-out-of-band split by LEDGER
   OWNERSHIP (any mirrored_secrets entry for the name, at any revision), not merely by whether
   the local row happens to be a tombstone. A live local row this node already owns but whose
   ciphertext no longer matches the newly shipped revision (e.g. an earlier tombstone-recovery
   re-stamp landed ahead of a since-shipped central UpdatedUtc, under site/central clock skew)
   is now re-stamped and re-applied exactly like a tombstone, instead of being misreported as
   someone else's "newer local value" and left permanently un-converged. The out-of-band Failed
   message is now only ever produced for a name that is NOT in the ledger. New test:
   Reconcile_ClockSkewedRecoveredRow_StillConvergesOnNextCentralRevision (a skewed future
   re-stamp followed by a genuinely new central revision converges and re-invalidates the
   cache).
2. Real-cipher integration test. Added DeployedSecretsMirrorRealStackTests, running the mirror
   against the REAL ZB.MOM.WW.Secrets stack (SqliteSecretStore + AesGcmEnvelopeCipher +
   DefaultSecretResolver over a real temp-file SQLite database, plus a hand-written
   IMasterKeyProvider test double mirroring the library's own) instead of in-memory fakes:
   encrypt -> SecretArtifact -> apply -> delete (tombstone) -> re-add the same artifact -> the
   recovered row resolves to the original plaintext. Pins the "UpdatedUtc/Revision are not
   AEAD-bound" property against the real cipher rather than by source inspection alone.
   Test-only package reference: ZB.MOM.WW.Secrets (SiteRuntime itself still depends only on
   ZB.MOM.WW.Secrets.Abstractions).

Verify: dotnet test tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests --filter FullyQualifiedName~DeployedSecretsMirrorTests (25/25); full SiteRuntime.Tests suite 878/878.
2026-09-28 19:19:22 -04:00
Joseph Doherty 4d7d6456e3 fix(site): DeployedSecretsMirror — confirm-then-ledger LWW apply, per-row isolation, cache invalidation
Fix round 1 (review task-9-review.md), all rulings:

1. (Important 1) ApplyReplicatedAsync is no longer trusted blindly. Every apply is
   confirmed by a store.GetAsync read-back before ledgering: a tombstoned local row
   (routine remove-then-re-add) is safely re-stamped past the tombstone and re-applied
   (confirmed neither UpdatedUtc nor Revision is AEAD-bound in AesGcmEnvelopeCipher, so
   the bump cannot break decryption); a live local row with different ciphertext (a
   strictly-newer out-of-band secret) is left untouched, never ledgered, never verified
   against, and reported with a distinct Failed message so a later central tombstone can
   never delete it via this mirror.
2. (Important 2) Per-row try/catch (rethrow only on ct.IsCancellationRequested) so one
   poisoned row (e.g. an unrecognised ContentType from a newer central) no longer blocks
   every row after it.
3. (Important 3) Optional ISecretCacheInvalidator, invalidated after every successful
   apply/delete and before verify, so a rotated/removed secret's plaintext cannot be
   served stale for the remainder of the resolver's TTL.
4. (Minor 4) Loop and VerifyAsync OperationCanceledException handling now filters on
   ct/stoppingToken.IsCancellationRequested specifically, rather than swallowing every OCE.
5. (Minor 5) Class remarks note the mirror assumes a node-local ISecretStore; Task 11
   will reject SqlServer secrets replication on the Site role.
6. (Minor 6/7) Added cancellation + concurrent-callers tests, dropped the redundant
   FailFor (tests now use ThrowFor/ReturnNullFor to exercise distinct decrypt-failure
   paths), and Dispose() now disposes the gate SemaphoreSlim.

Verify: dotnet test tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests --filter FullyQualifiedName~DeployedSecretsMirrorTests (24/24); full SiteRuntime.Tests suite 876/876.
2026-09-28 19:07:48 -04:00
Joseph Doherty 25c25ccef9 feat(site): DeployedSecretsMirror — per-node apply + decrypt-verify of deployed secrets 2026-09-28 18:52:44 -04:00