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; } }