using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using ZB.MOM.WW.Auth.Abstractions.ApiKeys; using ZB.MOM.WW.Auth.ApiKeys.DependencyInjection; namespace ZB.MOM.WW.Auth.ApiKeys.SqlServer.DependencyInjection; /// /// Dependency-injection entry point that swaps the API-key store backend from the SQLite default /// to a shared SQL Server database — one row set read and written by every node of a cluster (e.g. /// a ScadaBridge central pair), instead of the per-node file alone /// would give each node. /// public static class SqlServerApiKeyServiceCollectionExtensions { /// /// Registers the SQL Server-backed , , /// and , then calls /// to complete the registration /// (verifier, pepper provider, migration hosted service). /// /// /// /// Call this INSTEAD of , /// never both — this method calls it internally. The SQL Server stores and migrator are /// registered here, BEFORE that inner call, precisely because /// registers everything with /// TryAdd*: whichever implementation of (etc.) is registered /// first wins, and a later TryAdd for the same service is a no-op. That ordering is the /// entire mechanism by which this method displaces the SQLite default. /// /// /// still registers a SQLite /// connection factory from ApiKeyOptions.SqlitePath even when SQL Server wins every /// store slot. That is harmless — the SQLite factory is lazy and the connection it would open /// is never opened when nothing resolves it — and it is actually useful: it keeps the one path /// a node's pre-existing per-node SQLite file lives at in a single option, which is exactly the /// source path needs for the one-shot copy into the /// shared store. /// /// /// is registered here so a host can resolve it and run /// it once at boot (e.g. from a startup task or its own hosted service) to copy an existing /// node's per-node keys and audit rows into the now-shared store; it performs no work by being /// merely registered. /// /// /// The connection string must be a literal value or an environment-variable override — never /// a ${secret:} reference on a host whose secret store is itself SQL-backed: resolving /// that reference would require a working secret store, which on such a host requires this very /// database to already be reachable, an unbreakable circular dependency at boot. /// /// /// The service collection to add to. /// The application configuration. /// Path of the configuration section holding the core API-key options. /// Path of the configuration section holding . /// The same instance, for chaining. public static IServiceCollection AddZbApiKeyAuthSqlServer( this IServiceCollection services, IConfiguration config, string apiKeySectionPath, string sqlServerSectionPath) { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(config); ArgumentException.ThrowIfNullOrWhiteSpace(apiKeySectionPath); ArgumentException.ThrowIfNullOrWhiteSpace(sqlServerSectionPath); services.Configure(config.GetSection(sqlServerSectionPath)); services.TryAddSingleton(sp => new AuthSqlServerConnectionFactory(sp.GetRequiredService>().Value)); // Registered BEFORE AddZbApiKeyAuth so its TryAdd calls find these already present and // leave them alone — that is the whole mechanism by which the SQL Server store displaces // SQLite (see the class remarks). services.TryAddSingleton(sp => new SqlServerApiKeyStore(sp.GetRequiredService())); services.TryAddSingleton(sp => new SqlServerApiKeyAdminStore(sp.GetRequiredService())); services.TryAddSingleton(sp => new SqlServerApiKeyAuditStore(sp.GetRequiredService())); services.TryAddSingleton(); services.TryAddSingleton(sp => sp.GetRequiredService()); // Registered so a host can resolve it and run the one-shot per-node SQLite -> shared store // copy at boot; registering it here does no work on its own. services.TryAddSingleton(); return services.AddZbApiKeyAuth(config, apiKeySectionPath); } }