using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; namespace ZB.MOM.WW.Secrets.Replicator.Grpc.DependencyInjection; /// /// Dependency-injection and endpoint wiring for hosting the central secrets hub. /// /// /// /// Register this on the central node only. A site node registers the client half instead — /// the two are separate registrations on purpose, so "central hosts, sites follow" is visible in /// each role's composition root rather than inferred from configuration. /// /// /// Every node must be configured with the same KEK. The hub serves ciphertext, so a follower /// whose master key differs cannot decrypt what it pulls; it fails closed on resolve with a /// kek_id mismatch, which reads like data corruption but is a deployment error. /// /// public static class SecretsGrpcHubServerExtensions { /// /// Binds and validates , registers the fail-closed /// bearer interceptor, and attaches it to . /// /// /// Validation runs here, eagerly, as well as through the options pipeline: a hub with no bearer /// token should refuse to start rather than start and refuse every follower. The interceptor's /// deny-all is the second layer, for the case where a host constructs the options some other way. /// The service requires an ISecretStore in the container — register the store first. /// /// The service collection to add to. /// The application configuration. /// Configuration section holding the hub options. /// The same instance, for chaining. /// /// No bearer token is configured, or the hub has already been registered. /// public static IServiceCollection AddZbSecretsGrpcHub( this IServiceCollection services, IConfiguration configuration, string sectionPath = "Secrets:GrpcHub") { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(configuration); ArgumentException.ThrowIfNullOrWhiteSpace(sectionPath); // Registering twice is refused rather than tolerated, because the two calls do NOT compose: // options binding MERGES (a key absent from the second section leaves the first section's // value standing) while the SectionPath stamp below is last-one-wins. A hub registered from // two sections could therefore serve a token bound from one of them while every failure // message names the other — a misdirection that outlives the mistake that caused it. if (services.Any(descriptor => descriptor.ServiceType == typeof(HubRegistrationMarker))) { throw new InvalidOperationException( "The gRPC secrets hub may only be registered once; AddZbSecretsGrpcHub was called " + "more than once. Remove the duplicate call — a second registration would merge its " + "configuration into the first and leave the reported section path pointing at " + "whichever call ran last."); } // Registered as an instance so nothing ever has to activate it: the marker exists to be // looked for, not resolved. services.AddSingleton(new HubRegistrationMarker()); IConfigurationSection section = configuration.GetSection(sectionPath); services.Configure(section); // SectionPath is internal, so binding cannot set it; stamping it here is what lets the // startup failure and the runtime warning both name the key this host actually reads. services.PostConfigure(options => options.SectionPath = sectionPath); var eager = new SecretsGrpcHubServerOptions { SectionPath = sectionPath }; section.Bind(eager); eager.SectionPath = sectionPath; eager.Validate(); // Singleton so the "unconfigured" warning is emitted once per process rather than once per // interceptor instance; the gRPC interceptor activator prefers a registered instance. services.TryAddSingleton(); // AddGrpc is safe to call again if the host already called it — it registers with TryAdd. services.AddGrpc() .AddServiceOptions( options => options.Interceptors.Add()); return services; } /// /// Maps the secrets hub gRPC endpoint. The host must have set up routing; /// supplies the gRPC services and the auth interceptor. /// /// The endpoint route builder. /// The same instance, for chaining. public static IEndpointRouteBuilder MapZbSecretsHub(this IEndpointRouteBuilder endpoints) { ArgumentNullException.ThrowIfNull(endpoints); endpoints.MapGrpcService(); return endpoints; } /// /// Presence of this in the container means the hub is already registered. It carries no state /// and is never resolved — a dedicated type is used rather than probing for one of the real /// registrations because every one of those is added with TryAdd-style semantics that a /// host may legitimately have satisfied for its own reasons. /// private sealed class HubRegistrationMarker; }