43e87a7492f48e479a3191a3e9ae5fe339859352
3 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
43e87a7492 |
feat(secrets): central's Grpc-mode store is the SHARED SQL-Server store (scadaproj#4)
In Secrets:Replication:Mode=Grpc a central node's ISecretStore is now the
shared SQL-Server store (AddZbSecretsSqlServerStore) instead of a per-node
local SQLite store. Both central hubs read and write ONE copy of every row,
so they serve identical manifests by construction — the 2026-08-07 live gate
observed central-b answering an authenticated GetManifest with an EMPTY
manifest while central-a held every secret, which would turn site-side hub
failover into a silent convergence stop.
- SecretsRegistration: two fail-closed pre-checks before any registration on
the central+Grpc path — a blank Secrets:SqlServer:ConnectionString throws
naming the key (an independent store per central node is the recorded
defect), and a value containing ${secret: throws naming the bootstrap
circularity (the expander needs this store to resolve references). Site
registrations are byte-identical to before; SqlServer mode and
replication-off are untouched.
- Program.cs Layer-A expander follows the store swap: central+Grpc with a
non-blank connection string migrates and resolves pre-host ${secret:}
references through the shared SQL store, so expanded values can never
diverge from what the running node serves. Every other case keeps the
SQLite path unchanged; blank-connstr central deliberately falls through so
the clear AddScadaBridgeSecrets message is the one that fails the boot.
- appsettings.json: Secrets:SqlServer _comment now documents the Grpc-mode
central requirement (literal/env value only, sites leave it empty).
- SecretsReplicationWiringTests: +5 pins (shared store resolves, blank and
${secret:} connstrings fail naming the key, sites-have-no-SqlServer-types
descriptor sweep), central fixtures carry the now-required connstr.
Full suite green (7,474 passed across 30 projects, 0 warnings); the two
failures are pre-existing and unrelated: the Playwright live-rig suite fails
identically on unmodified main (cluster not running), and
GrpcCentralTransportTests.DeadlineExceeded_IsNotRetriedOnThePeer is a timing
flake that passes 3/3 in isolation and 470/470 on the first run of this code.
Claude-Session: https://claude.ai/code/session_014WNM4vjoVksyyBraTXSZE1
|
||
|
|
c8e90daafb |
test(secrets): pin both hub halves, the mode key, and the mapping gate
Extends the existing build-and-resolve wiring suite rather than asserting over ServiceDescriptors, for the reason that file already documents: a registration can look correct as a descriptor list and still fail on first resolve. Role separation is pinned in both directions — central registers the hub's fail-closed interceptor and NOT the sweep, a site registers the sweep and NOT the hub — because only one of those is a security failure and testing the happy half would not catch it. The sweep is asserted by resolving IHostedService, which builds its whole graph (reader, keyed channel, local store) and is where a forgotten AddZbSecrets would surface; GrpcSecretSyncService is internal to the package, so it is matched by assembly + type name the way the SqlServer replicator's services already are. SqlServer mode gets regression pins with the mode key both unset and named explicitly, plus one asserting it stays role-agnostic — both nodes sync bidirectionally against the same database, and the role parameter added for the hub must not have quietly changed that. The mapping tests assert over the app's real endpoint data sources, on the WIRE route (/zb.mom.ww.secrets.hub.v1.SecretsHub/...) rather than the C# type, since the route is what a follower addresses. One of them pins exactly the two READ methods: pull-only is a property of the contract, and this is where a future package version growing a write RPC would become visible instead of silently opening a path for a site to overwrite central. Numeric mode values get their own test. Enum.TryParse accepts any integer, including ones outside the enum, so "7" would otherwise select a mode that does not exist and fall through to the SqlServer branch. Verified red-first by mutation on the finished implementation: swapping the two role branches reds 8 (both role pins, both fail-closed pins, both mapping pins); deleting the UsesGrpcHub check in the map extension reds exactly the two "maps no hub endpoint" cases — the unauthenticated-hub scenario; dropping Enum.IsDefined and letting UsesGrpcHub ignore Enabled reds the out-of-range value and the flag-off-with-full-hub-config cases. 31/31 green restored. Claude-Session: https://claude.ai/code/session_014WNM4vjoVksyyBraTXSZE1 |
||
|
|
8e12f99432 |
feat(secrets): opt-in SQL-Server hub replication for the host secret store
Routes both host-container secret registrations (central role in Program.cs,
site role in SiteServiceRegistration) through a new SecretsRegistration
composition seam that optionally enables ZB.MOM.WW.Secrets.Replicator.SqlServer
hub-replication mode: each node keeps a LOCAL store that syncs bidirectionally
with a shared central SQL hub, so a site cluster keeps resolving secrets
straight through a WAN outage to central.
OPT-IN GATE (the load-bearing part). AddZbSecretsSqlServerReplication validates
its options EAGERLY at registration time, so wiring it unconditionally would
make ScadaBridge fail to START anywhere Secrets:SqlServer:ConnectionString is
unset -- every dev box, every docker node, every existing deployment. The
SQL-Server package is therefore only touched when BOTH Secrets:Replication:
Enabled is true AND a non-blank connection string is present; otherwise the
registration is byte-identical to the previous plain AddZbSecrets call.
Enabled-without-a-connection-string falls back to local-only and logs a warning
rather than failing the node or silently looking healthy.
BOOTSTRAP CYCLE. The hub connection string is itself a secret and can never come
from the hub -- a node cannot read the hub to learn how to reach the hub. It must
arrive from outside the replicated set: an environment variable, or a ${secret:}
reference seeded in that node's own LOCAL store. appsettings.json therefore ships
ConnectionString empty with a _comment saying so (leaf keys starting with '_' are
skipped by the reference expander, verified in SecretReferenceExpander). No real
connection string is committed. The pre-host ${secret:} expander in Program.cs is
deliberately left on a plain local SQLite store for the same reason.
Per-node docker appsettings are intentionally NOT modified -- replication stays
off there for now.
Tests written before the wiring and confirmed red first (2 failed / 4 passed),
green after (6/6). They BUILD a container and RESOLVE from it rather than
asserting over ServiceDescriptors: a decorator can look correct as a descriptor
list and still throw on first resolve because the undecorated concrete store it
depends on is missing -- that exact defect shipped once in this library with all
descriptor-level tests green, so the undecorated SqliteSecretStore gets its own
resolution test.
Verified: Host.Tests 285/285 pass; full build (all projects except the
pre-existing AngleSharp NU1902 CentralUI.Tests restore break) 0 warnings,
0 errors.
Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
|