using ZB.MOM.WW.Secrets.DependencyInjection; using ZB.MOM.WW.Secrets.Replicator.SqlServer.DependencyInjection; namespace ZB.MOM.WW.ScadaBridge.Host; /// /// Single composition-root entry point for the host container's secret store, shared by the /// central-role registrations in Program.cs and the site-role registrations in /// so both roles wire secrets identically. /// /// /// /// Clustered replication is opt-in and off by default. ScadaBridge is hub-and-spoke — one /// central cluster plus N separate site clusters — and hub mode gives each node a LOCAL store that /// syncs bidirectionally with a shared central SQL hub, so a site keeps resolving secrets straight /// through a WAN outage. /// /// /// The gate is not a style preference. AddZbSecretsSqlServerReplication validates its options /// EAGERLY at registration time, so calling it unconditionally would throw at startup on every node /// where Secrets:SqlServer:ConnectionString is unset — which is every dev box, every docker /// node and every existing deployment. Both the explicit Secrets:Replication:Enabled flag and /// a non-blank connection string are therefore required before the SQL-Server package is touched at /// all; otherwise this is byte-for-byte the plain local-SQLite registration it has always been. /// /// /// Bootstrap constraint. The hub connection string is itself a secret, and it 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 that is /// seeded in that node's own LOCAL store (the pre-host expander in Program.cs runs against a /// plain local SQLite store before the host container exists, so such a reference does resolve). /// That expander is deliberately left un-replicated for exactly this reason. /// /// public static class SecretsRegistration { /// Configuration section holding the core secrets options. public const string SecretsSectionPath = "Secrets"; /// Configuration section holding the SQL-Server hub options. public const string SqlServerSectionPath = "Secrets:SqlServer"; /// Configuration key gating clustered replication. Absent or false = off. public const string ReplicationEnabledKey = "Secrets:Replication:Enabled"; /// Configuration key holding the shared SQL-Server hub connection string. public const string HubConnectionStringKey = "Secrets:SqlServer:ConnectionString"; /// /// Registers the host container's secret store: a plain local SQLite store by default, or a /// local store replicating against a shared SQL-Server hub when replication is explicitly /// enabled and a hub connection string is present. /// /// The service collection to register into. /// Application configuration for options binding. /// The same instance, for chaining. public static IServiceCollection AddScadaBridgeSecrets( this IServiceCollection services, IConfiguration config) { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(config); var enabled = config.GetValue(ReplicationEnabledKey); var connectionString = config[HubConnectionStringKey]; if (enabled && !string.IsNullOrWhiteSpace(connectionString)) { // Registers the validated hub options, decorates ISecretStore with // ReplicatingSecretStore, and adds the hub-schema migration + bidirectional sweep // hosted services. Calls AddZbSecrets internally — do NOT also call it here. return services.AddZbSecretsSqlServerReplication( config, SecretsSectionPath, SqlServerSectionPath); } if (enabled) { // Asked for replication but gave nothing to replicate against. Falling back to the // local-only store keeps the node up, but it is silently NOT participating in the // cluster, so say so loudly rather than letting it look healthy. Serilog.Log.Warning( "{Key} is true but {ConnKey} is empty — clustered secret replication is DISABLED " + "and this node's secrets are local-only. The hub connection string cannot come " + "from the hub itself; supply it via environment variable or seed it in this " + "node's local store.", ReplicationEnabledKey, HubConnectionStringKey); } return services.AddZbSecrets(config, SecretsSectionPath); } }