feat(secrets)!: retire the gRPC secrets hub in ScadaBridge — sites get secrets via Deploy Artifacts; rig central on the shared SQL store

This commit is contained in:
Joseph Doherty
2026-09-28 19:52:26 -04:00
parent 52cd4fb096
commit 2bac13170c
11 changed files with 333 additions and 1181 deletions
@@ -771,23 +771,58 @@ public class StartupValidatorTests
Assert.Empty(warnings);
}
[Fact]
public void ClusteredCentral_ReplicationEnabled_NoSecretsStoreWarning()
/// <summary>
/// Task 11: secrets replication (the gRPC hub, and the SQL-Server ReplicatingSecretStore mode
/// that preceded it) is retired in ScadaBridge — sites receive secrets exclusively via Deploy
/// Artifacts, and a replicated site store would publish DeployedSecretsMirror's local deletes
/// fleet-wide. Checked on BOTH roles: a Site used to be the gRPC hub's follower half, so the
/// flag was never Central-only. Replaces the old warning-suppression test
/// (ClusteredCentral_ReplicationEnabled_NoSecretsStoreWarning) — that suppression is gone too,
/// because CollectWarnings never runs with this flag true on a config that also passed
/// Validate.
/// </summary>
[Theory]
[InlineData("true")]
[InlineData("True")]
public void ReplicationEnabled_IsRejected_OnCentral(string value)
{
// Review round 1, finding #1: Secrets:Replication:Enabled=true already puts this central
// node on a topology that shares state across the pair — the pre-existing gRPC hub
// (whose central-side store is the shared SQL-Server store, not per-node SQLite) or
// SQL-Server replication. No Secrets:Store:Provider is set at all here — the exact case
// that used to warn before this fix — and it must stay silent because the node is not
// actually on a per-node SQLite store in this configuration.
var values = ValidCentralConfig();
values["ScadaBridge:InboundApi:ApiKeyStore:Provider"] = "SqlServer";
values["Secrets:Replication:Enabled"] = value;
var config = BuildConfig(values);
var ex = Assert.Throws<InvalidOperationException>(() => StartupValidator.Validate(config));
Assert.Contains(
"Secrets replication (the gRPC hub) is retired in ScadaBridge", ex.Message);
Assert.Contains("sites receive secrets with Deploy Artifacts", ex.Message);
Assert.Contains("central uses Secrets:Store:Provider=SqlServer", ex.Message);
}
[Fact]
public void ReplicationEnabled_IsRejected_OnSite()
{
var values = ValidSiteConfig();
values["Secrets:Replication:Enabled"] = "true";
var config = BuildConfig(values);
var warnings = StartupValidator.CollectWarnings(config);
var ex = Assert.Throws<InvalidOperationException>(() => StartupValidator.Validate(config));
Assert.Empty(warnings);
Assert.Contains(
"Secrets replication (the gRPC hub) is retired in ScadaBridge", ex.Message);
}
[Fact]
public void ReplicationDisabled_PassesValidation()
{
// Explicit false must not be confused with "set" — the default posture every existing
// deployment ships stays valid.
var values = ValidCentralConfig();
values["Secrets:Replication:Enabled"] = "false";
var config = BuildConfig(values);
var ex = Record.Exception(() => StartupValidator.Validate(config));
Assert.Null(ex);
}
[Fact]