feat(secrets): SecretsOptions + AddZbSecrets DI + migration hosted service
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
using ZB.MOM.WW.Secrets.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.Secrets.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// The single-node default <see cref="ISecretReplicator"/>: publishing a row is a no-op because
|
||||
/// there are no peers to broadcast to. The deferred <c>ZB.MOM.WW.Secrets.Akka</c> package replaces
|
||||
/// this registration with a real cluster replicator for clustered deployments.
|
||||
/// </summary>
|
||||
public sealed class NoOpSecretReplicator : ISecretReplicator
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Task PublishAsync(StoredSecret row, CancellationToken ct) => Task.CompletedTask;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZB.MOM.WW.Secrets.Sqlite;
|
||||
|
||||
namespace ZB.MOM.WW.Secrets.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Runs the secrets SQLite schema migration at application startup when
|
||||
/// <see cref="SecretsOptions.RunMigrationsOnStartup"/> is <see langword="true"/>. The migration is
|
||||
/// idempotent, so repeated restarts are safe.
|
||||
/// </summary>
|
||||
internal sealed class SecretsMigrationHostedService(
|
||||
SqliteSecretsStoreMigrator migrator,
|
||||
IOptions<SecretsOptions> options) : IHostedService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (options.Value.RunMigrationsOnStartup)
|
||||
{
|
||||
await migrator.MigrateAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZB.MOM.WW.Audit;
|
||||
using ZB.MOM.WW.Secrets.Abstractions;
|
||||
using ZB.MOM.WW.Secrets.Crypto;
|
||||
using ZB.MOM.WW.Secrets.MasterKey;
|
||||
using ZB.MOM.WW.Secrets.Sqlite;
|
||||
|
||||
namespace ZB.MOM.WW.Secrets.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Dependency-injection helpers that wire the ZB.MOM.WW secrets subsystem — master-key provider,
|
||||
/// envelope cipher, SQLite store, resolver, no-op replicator, and the startup migration hosted
|
||||
/// service — from configuration with a single call.
|
||||
/// </summary>
|
||||
public static class SecretsServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers the secrets subsystem: binds <see cref="SecretsOptions"/> from the configuration
|
||||
/// section at <paramref name="sectionPath"/> and wires the master-key provider, envelope cipher,
|
||||
/// SQLite-backed store, the caching resolver, the single-node no-op replicator, and a hosted
|
||||
/// service that runs the schema migration on startup (when enabled).
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection to add to.</param>
|
||||
/// <param name="config">The application configuration.</param>
|
||||
/// <param name="sectionPath">Path of the configuration section holding the secrets options.</param>
|
||||
/// <returns>The same <paramref name="services"/> instance, for chaining.</returns>
|
||||
public static IServiceCollection AddZbSecrets(
|
||||
this IServiceCollection services,
|
||||
IConfiguration config,
|
||||
string sectionPath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(sectionPath);
|
||||
|
||||
services.Configure<SecretsOptions>(config.GetSection(sectionPath));
|
||||
|
||||
// One connection factory targets the configured SQLite path. Singleton: it is stateless
|
||||
// aside from the path and opens a fresh connection per operation.
|
||||
services.TryAddSingleton(sp =>
|
||||
new SecretsSqliteConnectionFactory(
|
||||
sp.GetRequiredService<IOptions<SecretsOptions>>().Value.SqlitePath));
|
||||
|
||||
services.TryAddSingleton<IMasterKeyProvider>(sp =>
|
||||
MasterKeyProviderFactory.Create(
|
||||
sp.GetRequiredService<IOptions<SecretsOptions>>().Value.MasterKey));
|
||||
|
||||
services.TryAddSingleton<ISecretCipher>(sp =>
|
||||
new AesGcmEnvelopeCipher(sp.GetRequiredService<IMasterKeyProvider>()));
|
||||
|
||||
services.TryAddSingleton<ISecretStore>(sp =>
|
||||
new SqliteSecretStore(sp.GetRequiredService<SecretsSqliteConnectionFactory>()));
|
||||
|
||||
services.TryAddSingleton<ISecretReplicator, NoOpSecretReplicator>();
|
||||
|
||||
services.TryAddSingleton<ISecretResolver>(sp =>
|
||||
{
|
||||
// Resolve the audit writer LAZILY so this picks up the app's AddZbAudit registration
|
||||
// regardless of call order; fall back to a no-op writer when the app registers none.
|
||||
IAuditWriter audit = sp.GetService<IAuditWriter>() ?? NoOpAuditWriter.Instance;
|
||||
ISecretActorAccessor? actorAccessor = sp.GetService<ISecretActorAccessor>();
|
||||
TimeProvider? timeProvider = sp.GetService<TimeProvider>();
|
||||
TimeSpan ttl = sp.GetRequiredService<IOptions<SecretsOptions>>().Value.ResolveCacheTtl;
|
||||
|
||||
return new DefaultSecretResolver(
|
||||
sp.GetRequiredService<ISecretStore>(),
|
||||
sp.GetRequiredService<ISecretCipher>(),
|
||||
audit,
|
||||
ttl,
|
||||
actorAccessor,
|
||||
timeProvider);
|
||||
});
|
||||
|
||||
// Migrator: singleton, constructed from the already-registered connection factory. Needed
|
||||
// before any store operations so the schema exists.
|
||||
services.TryAddSingleton(sp =>
|
||||
new SqliteSecretsStoreMigrator(sp.GetRequiredService<SecretsSqliteConnectionFactory>()));
|
||||
|
||||
// Hosted service that runs migrations on startup when SecretsOptions.RunMigrationsOnStartup.
|
||||
services.AddHostedService<SecretsMigrationHostedService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using ZB.MOM.WW.Secrets.MasterKey;
|
||||
|
||||
namespace ZB.MOM.WW.Secrets;
|
||||
|
||||
/// <summary>
|
||||
/// Application-level configuration for the ZB.MOM.WW secrets subsystem: where the SQLite store
|
||||
/// lives, how the master key (KEK) is resolved, whether the schema migration runs on startup,
|
||||
/// how long decrypted values are cached, and whether the UI reveal path is enabled. Bound from a
|
||||
/// configuration section by <see cref="DependencyInjection.SecretsServiceCollectionExtensions.AddZbSecrets"/>.
|
||||
/// </summary>
|
||||
public sealed class SecretsOptions
|
||||
{
|
||||
/// <summary>The path to the SQLite database backing the secret store. Defaults to <c>secrets.db</c>.</summary>
|
||||
public string SqlitePath { get; set; } = "secrets.db";
|
||||
|
||||
/// <summary>The master-key (KEK) resolution configuration. Defaults to a fresh <see cref="MasterKeyOptions"/>.</summary>
|
||||
public MasterKeyOptions MasterKey { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// When <see langword="true"/> (the default), the SQLite schema migration runs at application
|
||||
/// startup via the hosted service. The migration is idempotent, so repeated restarts are safe.
|
||||
/// </summary>
|
||||
public bool RunMigrationsOnStartup { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// How long a decrypted secret value is cached in process memory by the resolver. Kept short by
|
||||
/// default (30 seconds) because the cache holds plaintext secret material.
|
||||
/// </summary>
|
||||
public TimeSpan ResolveCacheTtl { get; set; } = TimeSpan.FromSeconds(30);
|
||||
|
||||
/// <summary>
|
||||
/// Opt-in switch for the UI reveal path (consumed by the deferred <c>ZB.MOM.WW.Secrets.Ui</c>
|
||||
/// package). Defaults to <see langword="false"/> — secrets are write-only from the UI unless a
|
||||
/// deployment explicitly opts in.
|
||||
/// </summary>
|
||||
public bool RevealEnabled { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user