using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Communication;
using ZB.MOM.WW.ScadaBridge.Communication.Grpc;
using ZB.MOM.WW.Secrets;
using ZB.MOM.WW.Secrets.Abstractions;
namespace ZB.MOM.WW.ScadaBridge.Host;
///
/// Central's : forwards every eviction to the secret
/// resolver's cache as before, and when the evicted secret is a site's gRPC preshared key
/// (SB-GRPC-PSK-{siteId}) also drops that site's entry from
/// and tells the peer central node to do the same (Gitea #96).
///
///
///
/// Why the invalidator seam. Every ZB.MOM.WW.Secrets write path — the
/// /admin/secrets editor and delete modal — calls
/// right after a rotate or delete. Hooking that seam catches an in-place PSK rotation at the
/// moment it is written, without ScadaBridge owning a write path of its own.
///
///
/// Why the peer needs telling. Both central nodes cache the key in their own
/// (and their own resolver cache), and the write only ran on one of
/// them. The peer is told with a on the central cache-invalidation
/// topic (, ScadaBridge#100 item 3, which generalised
/// the #96 site-psk-invalidation topic); this class is also the peer's handler for it.
///
///
/// Writes made outside this process (the secret CLI, a direct SQL write) never reach this
/// seam; the site's rejection of the stale key covers those (see
/// ControlPlaneCredentials.WithSiteCredentials).
///
///
public sealed class SitePskRotationInvalidator : ISecretCacheInvalidator, ICentralCacheInvalidationHandler
{
private readonly ISecretCacheInvalidator _inner;
private readonly ISitePskProvider _pskProvider;
private readonly ICentralCacheInvalidationPublisher? _publisher;
private readonly ILogger _logger;
/// Creates the invalidator.
/// The library's own invalidator (the secret resolver's cache).
/// Central's per-site PSK cache.
/// The central cache-invalidation publisher; null keeps evictions local.
/// Logger.
public SitePskRotationInvalidator(
ISecretCacheInvalidator inner,
ISitePskProvider pskProvider,
ICentralCacheInvalidationPublisher? publisher,
ILogger logger)
{
ArgumentNullException.ThrowIfNull(inner);
ArgumentNullException.ThrowIfNull(pskProvider);
ArgumentNullException.ThrowIfNull(logger);
_inner = inner;
_pskProvider = pskProvider;
_publisher = publisher;
_logger = logger;
}
///
public void Invalidate(SecretName name)
{
_inner.Invalidate(name);
var siteId = SitePskProvider.SiteIdFromSecretName(name.Value);
if (siteId is null)
{
return;
}
_pskProvider.Invalidate(siteId);
_logger.LogInformation(
"gRPC preshared key secret '{SecretName}' was written; dropped the cached key for site "
+ "{SiteId} and notified the peer central node.", name.Value, siteId);
_publisher?.Publish(new SitePskInvalidated(siteId));
}
///
public void Apply(ICentralCacheInvalidation invalidation)
{
if (invalidation is SitePskInvalidated psk)
{
ApplyPeerInvalidation(psk.SiteId);
}
}
///
/// Applies an invalidation received from the peer central node: evicts the resolver's cached
/// secret and the PSK cache entry. Never re-publishes.
///
/// The site whose key was rotated.
public void ApplyPeerInvalidation(string siteId)
{
if (string.IsNullOrWhiteSpace(siteId))
{
return;
}
_inner.Invalidate(new SecretName(SitePskProvider.SecretNameFor(siteId)));
_pskProvider.Invalidate(siteId);
}
}
///
/// Peer message: the gRPC preshared key of was rotated on a central node.
/// Rides the central cache-invalidation topic; kept in this namespace because the #96
/// site-psk-invalidation topic carries it bare by this type name (see
/// ).
///
/// The site whose key changed.
public sealed record SitePskInvalidated(string SiteId) : ICentralCacheInvalidation;
/// DI wiring for .
public static class SitePskRotationServiceCollectionExtensions
{
///
/// Replaces the secrets library's with
/// wrapping the library's resolver, and registers it
/// as a central cache-invalidation handler. Central only; call after
/// AddScadaBridgeSecrets, after is registered and after
/// .
///
/// The service collection.
/// The same collection.
public static IServiceCollection AddSitePskRotationInvalidation(this IServiceCollection services)
{
services.TryAddSingleton(sp => new SitePskRotationInvalidator(
sp.GetRequiredService(),
sp.GetRequiredService(),
sp.GetService(),
sp.GetRequiredService>()));
services.Replace(ServiceDescriptor.Singleton(
sp => sp.GetRequiredService()));
services.AddSingleton(
sp => sp.GetRequiredService());
return services;
}
}