fix(secrets): share the central connstr validation with the Layer-A expander
The blank/${secret:} pre-checks lived only in AddScadaBridgeSecrets, but on
a real central boot the Layer-A expander runs FIRST and would hand a bad
value to SqlConnection, burying the designed message under a generic
'initialization string' format error. Extracted both checks into
EnsureCentralSharedStoreConnectionString — one definition, called by the
expander (earliest point) and by registration (covers embedded/test
composition) — same single-source lesson as the UsesGrpcHub predicate.
Claude-Session: https://claude.ai/code/session_014WNM4vjoVksyyBraTXSZE1
This commit is contained in:
@@ -60,18 +60,19 @@ var configuration = new ConfigurationBuilder()
|
||||
// scadaproj#4) — an expander left on SQLite there would resolve pre-host ${secret:} references
|
||||
// from a stale/empty local store, silently diverging from what the node's own hub serves. Every
|
||||
// other case (sites, SqlServer mode, replication off) keeps the local SQLite path exactly as it
|
||||
// always was. Central+Grpc with a BLANK connection string deliberately falls through to the
|
||||
// SQLite path too: that boot is about to fail in AddScadaBridgeSecrets with the message naming
|
||||
// Secrets:SqlServer:ConnectionString, so the throw is not duplicated here.
|
||||
// always was. A blank or ${secret:}-valued connection string fails HERE, through the same
|
||||
// EnsureCentralSharedStoreConnectionString check AddScadaBridgeSecrets runs — this is the
|
||||
// earliest code that would otherwise hand the bad value to SqlConnection, whose generic
|
||||
// "initialization string" format error would bury the designed message.
|
||||
var expanderUsesSharedSqlStore =
|
||||
string.Equals(
|
||||
configuration["ScadaBridge:Node:Role"], "Central", StringComparison.OrdinalIgnoreCase)
|
||||
&& SecretsRegistration.UsesGrpcHub(configuration)
|
||||
&& !string.IsNullOrWhiteSpace(configuration[SecretsRegistration.HubConnectionStringKey]);
|
||||
&& SecretsRegistration.UsesGrpcHub(configuration);
|
||||
|
||||
var expanderServices = new ServiceCollection();
|
||||
if (expanderUsesSharedSqlStore)
|
||||
{
|
||||
SecretsRegistration.EnsureCentralSharedStoreConnectionString(configuration);
|
||||
expanderServices.AddZbSecretsSqlServerStore(configuration, "Secrets");
|
||||
}
|
||||
else
|
||||
|
||||
@@ -185,6 +185,59 @@ public static class SecretsRegistration
|
||||
&& ResolveReplicationMode(config) == SecretsReplicationMode.Grpc;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The one definition of "a Central node in Grpc mode has a usable shared-store connection
|
||||
/// string", shared by <see cref="AddScadaBridgeSecrets"/> and the Layer-A expander in
|
||||
/// <c>Program.cs</c> so the two can never diverge. The expander runs FIRST on a real boot —
|
||||
/// without this sharing, a bad value would die inside <c>SqlConnection</c> with a generic
|
||||
/// "initialization string" format error before the designed message ever fired.
|
||||
/// </summary>
|
||||
/// <param name="config">Application configuration.</param>
|
||||
/// <returns>The validated connection string.</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// <see cref="HubConnectionStringKey"/> is blank or contains a <c>${secret:}</c> reference.
|
||||
/// </exception>
|
||||
internal static string EnsureCentralSharedStoreConnectionString(IConfiguration config)
|
||||
{
|
||||
var hubConnectionString = config[HubConnectionStringKey];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(hubConnectionString))
|
||||
{
|
||||
// Fail closed, matching the mode's philosophy: central in Grpc mode REQUIRES the
|
||||
// shared SQL store. An independent local store per central node is exactly the
|
||||
// divergence scadaproj#4 recorded — one central hub answered authenticated followers
|
||||
// with an EMPTY manifest, so a hub failover would "succeed" against nothing and stop
|
||||
// convergence silently.
|
||||
throw new InvalidOperationException(
|
||||
$"{HubConnectionStringKey} is empty, but this node is Central with "
|
||||
+ $"{ReplicationModeKey}=Grpc. Central in Grpc mode requires the "
|
||||
+ "SHARED SQL-Server secret store — an independent local store per "
|
||||
+ "central node is the divergence scadaproj#4 recorded (a hub "
|
||||
+ "failover would 'succeed' against an empty manifest). Supply the "
|
||||
+ "connection string via appsettings or the environment "
|
||||
+ "(Secrets__SqlServer__ConnectionString).");
|
||||
}
|
||||
|
||||
if (hubConnectionString.Contains("${secret:", StringComparison.Ordinal))
|
||||
{
|
||||
// The secrets store's own connection string can never be a secret reference: the
|
||||
// pre-host expander needs the store this string points at in order to resolve it —
|
||||
// bootstrap circularity, the same rule the class remarks document for the hub bearer
|
||||
// token. The message deliberately does not echo the configured value: a mistyped
|
||||
// value here could be a pasted real credential.
|
||||
throw new InvalidOperationException(
|
||||
$"{HubConnectionStringKey} contains a ${{secret:}} reference. The "
|
||||
+ "secret store's own connection string can never be a secret "
|
||||
+ "reference — the pre-host expander needs the store this string "
|
||||
+ "points at in order to resolve it (bootstrap circularity, the "
|
||||
+ "same rule as Secrets:GrpcHub:BearerToken). Supply a literal "
|
||||
+ "value via appsettings or the environment "
|
||||
+ "(Secrets__SqlServer__ConnectionString).");
|
||||
}
|
||||
|
||||
return hubConnectionString;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the host container's secret store: a plain local SQLite store by default, or a
|
||||
/// replicating topology when <see cref="ReplicationEnabledKey"/> is set — a shared SQL-Server
|
||||
@@ -212,45 +265,11 @@ public static class SecretsRegistration
|
||||
switch (role)
|
||||
{
|
||||
case SecretsNodeRole.Central:
|
||||
// Both pre-checks run BEFORE any registration so the boot failure names the
|
||||
// real problem instead of surfacing later as a package validation message or a
|
||||
// malformed-connection-string fault on first use.
|
||||
var hubConnectionString = config[HubConnectionStringKey];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(hubConnectionString))
|
||||
{
|
||||
// Fail closed, matching the mode's philosophy: central in Grpc mode
|
||||
// REQUIRES the shared SQL store. An independent local store per central
|
||||
// node is exactly the divergence scadaproj#4 recorded — one central hub
|
||||
// answered authenticated followers with an EMPTY manifest, so a hub
|
||||
// failover would "succeed" against nothing and stop convergence silently.
|
||||
throw new InvalidOperationException(
|
||||
$"{HubConnectionStringKey} is empty, but this node is Central with "
|
||||
+ $"{ReplicationModeKey}=Grpc. Central in Grpc mode requires the "
|
||||
+ "SHARED SQL-Server secret store — an independent local store per "
|
||||
+ "central node is the divergence scadaproj#4 recorded (a hub "
|
||||
+ "failover would 'succeed' against an empty manifest). Supply the "
|
||||
+ "connection string via appsettings or the environment "
|
||||
+ "(Secrets__SqlServer__ConnectionString).");
|
||||
}
|
||||
|
||||
if (hubConnectionString.Contains("${secret:", StringComparison.Ordinal))
|
||||
{
|
||||
// The secrets store's own connection string can never be a secret
|
||||
// reference: the pre-host expander needs the store this string points at
|
||||
// in order to resolve it — bootstrap circularity, the same rule the class
|
||||
// remarks document for the hub bearer token. The message deliberately
|
||||
// does not echo the configured value: a mistyped value here could be a
|
||||
// pasted real credential.
|
||||
throw new InvalidOperationException(
|
||||
$"{HubConnectionStringKey} contains a ${{secret:}} reference. The "
|
||||
+ "secret store's own connection string can never be a secret "
|
||||
+ "reference — the pre-host expander needs the store this string "
|
||||
+ "points at in order to resolve it (bootstrap circularity, the "
|
||||
+ "same rule as Secrets:GrpcHub:BearerToken). Supply a literal "
|
||||
+ "value via appsettings or the environment "
|
||||
+ "(Secrets__SqlServer__ConnectionString).");
|
||||
}
|
||||
// Runs BEFORE any registration so the boot failure names the real problem
|
||||
// instead of surfacing later as a package validation message or a
|
||||
// malformed-connection-string fault on first use. On a real boot the Layer-A
|
||||
// expander in Program.cs has already run the same check even earlier.
|
||||
EnsureCentralSharedStoreConnectionString(config);
|
||||
|
||||
// Central's store is the SHARED SQL-Server store: registers
|
||||
// SqlServerSecretStore as ISecretStore BEFORE calling AddZbSecrets internally,
|
||||
|
||||
Reference in New Issue
Block a user