chore(secrets): wire cache invalidation on rotate/delete, drop dead RevealEnabled, remove scaffold leftovers
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
namespace ZB.MOM.WW.Secrets.Abstractions;
|
||||
|
||||
/// <summary>Evicts a cached decrypted secret so the next resolve re-reads it. Called by write paths after a rotate/delete.</summary>
|
||||
public interface ISecretCacheInvalidator
|
||||
{
|
||||
/// <summary>Evicts the cache entry for <paramref name="name"/> (no-op if absent).</summary>
|
||||
void Invalidate(SecretName name);
|
||||
}
|
||||
@@ -96,6 +96,11 @@
|
||||
|
||||
if (deleted)
|
||||
{
|
||||
// Evict any cached plaintext so the ${secret:} / ISecretResolver path does not serve a
|
||||
// stale value after this delete for the remainder of its TTL. Optional seam — a no-op
|
||||
// when the host has not registered an invalidator.
|
||||
Services.GetService<ISecretCacheInvalidator>()?.Invalidate(Name);
|
||||
|
||||
await OnDeleted.InvokeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +169,11 @@
|
||||
|
||||
if (saved)
|
||||
{
|
||||
// Evict any cached plaintext so the ${secret:} / ISecretResolver path does not serve a
|
||||
// stale value after this rotate/add for the remainder of its TTL. Optional seam — a no-op
|
||||
// when the host has not registered an invalidator.
|
||||
Services.GetService<ISecretCacheInvalidator>()?.Invalidate(name);
|
||||
|
||||
await OnSaved.InvokeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace ZB.MOM.WW.Secrets;
|
||||
|
||||
/// <summary>
|
||||
/// Internal marker giving the assembly a compilable input while the store and
|
||||
/// resolver are scaffolded. Replace with real implementation as it is added.
|
||||
/// </summary>
|
||||
internal static class AssemblyMarker
|
||||
{
|
||||
}
|
||||
@@ -32,7 +32,7 @@ namespace ZB.MOM.WW.Secrets;
|
||||
/// ciphertext and any value are never included.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class DefaultSecretResolver : ISecretResolver
|
||||
public sealed class DefaultSecretResolver : ISecretResolver, ISecretCacheInvalidator
|
||||
{
|
||||
private const string SystemActor = "system";
|
||||
private const string AuditCategory = "Secrets";
|
||||
@@ -125,8 +125,8 @@ public sealed class DefaultSecretResolver : ISecretResolver
|
||||
|
||||
/// <summary>
|
||||
/// Evicts the cached plaintext for <paramref name="name"/>, if any. The admin / write path
|
||||
/// calls this immediately after a rotate or delete so a stale value is not served for the
|
||||
/// remainder of its TTL. Not part of <see cref="ISecretResolver"/>; cheap and idempotent.
|
||||
/// calls this (via <see cref="ISecretCacheInvalidator"/>) immediately after a rotate or delete
|
||||
/// so a stale value is not served for the remainder of its TTL. Cheap and idempotent.
|
||||
/// </summary>
|
||||
/// <param name="name">The secret whose cache entry to remove.</param>
|
||||
public void Invalidate(SecretName name) => _cache.TryRemove(name.Value, out _);
|
||||
|
||||
+7
-1
@@ -56,7 +56,10 @@ public static class SecretsServiceCollectionExtensions
|
||||
|
||||
services.TryAddSingleton<ISecretReplicator, NoOpSecretReplicator>();
|
||||
|
||||
services.TryAddSingleton<ISecretResolver>(sp =>
|
||||
// ONE shared resolver instance backs both the read seam (ISecretResolver) and the
|
||||
// cache-invalidation seam (ISecretCacheInvalidator) so a write-path invalidation hits the
|
||||
// exact cache the resolver reads from.
|
||||
services.TryAddSingleton<DefaultSecretResolver>(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.
|
||||
@@ -74,6 +77,9 @@ public static class SecretsServiceCollectionExtensions
|
||||
timeProvider);
|
||||
});
|
||||
|
||||
services.TryAddSingleton<ISecretResolver>(sp => sp.GetRequiredService<DefaultSecretResolver>());
|
||||
services.TryAddSingleton<ISecretCacheInvalidator>(sp => sp.GetRequiredService<DefaultSecretResolver>());
|
||||
|
||||
// Migrator: singleton, constructed from the already-registered connection factory. Needed
|
||||
// before any store operations so the schema exists.
|
||||
services.TryAddSingleton(sp =>
|
||||
|
||||
@@ -4,8 +4,8 @@ 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
|
||||
/// lives, how the master key (KEK) is resolved, whether the schema migration runs on startup, and
|
||||
/// how long decrypted values are cached. Bound from a
|
||||
/// configuration section by <see cref="DependencyInjection.SecretsServiceCollectionExtensions.AddZbSecrets"/>.
|
||||
/// </summary>
|
||||
public sealed class SecretsOptions
|
||||
@@ -27,11 +27,4 @@ public sealed class SecretsOptions
|
||||
/// 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