namespace ZB.MOM.WW.Secrets.Replicator.Grpc; /// /// Configuration for the central-hosted secrets hub. Bound from a configuration section by /// AddZbSecretsGrpcHub. /// /// /// /// The hub serves ciphertext to followers that authenticate with one shared bearer token. That /// token is the whole of the hub's authorization: there is no per-follower identity, because a /// follower's authority is uniform (read the manifest, read rows) and the pull-only contract gives /// it nothing else to be authorized for. /// /// /// It cannot itself be supplied as a ${secret:} reference — resolving that reference is what /// the hub exists to make possible — so it comes from appsettings or the environment, the same way /// the family's mesh pre-shared keys do. /// /// public sealed class SecretsGrpcHubServerOptions { /// The configuration section the hub is bound from unless a host overrides it. internal const string DefaultSectionPath = "Secrets:GrpcHub"; /// /// The default ceiling on names in one GetSecrets call. Set comfortably above the /// follower's own fetch chunk so a compliant sweep never approaches it. /// internal const int DefaultMaxNamesPerRequest = 1000; /// /// The shared bearer token a follower must present. Required — an unset token is not "open /// access", it is a hub that refuses every call (see ). /// public string BearerToken { get; set; } = string.Empty; /// /// The most names a single GetSecrets call may carry. A larger request is refused with /// ResourceExhausted rather than served. /// /// /// The well-behaved follower already chunks its fetches well below this, so the cap is not a /// throughput knob — it is the server declining to let one authenticated caller turn a single /// request into an unbounded sequential store sweep. The cap sits behind authentication, so it /// is defence in depth against a misbehaving token holder, not an anonymous-DoS control. /// public int MaxNamesPerRequest { get; set; } = DefaultMaxNamesPerRequest; /// /// The configuration section this instance was bound from, used so failure messages name the /// key an operator actually has to set. /// /// /// Deliberately : configuration binding only writes public settable /// properties, so a stray SectionPath key in an appsettings file cannot make the hub /// misreport which key it wants. The registration extension sets it. /// internal string SectionPath { get; set; } = DefaultSectionPath; /// /// Throws if the options are unusable. Called by the registration extension so a hub with no /// token fails at startup, rather than starting and rejecting every follower at runtime. /// /// /// The bearer token is missing or blank, or the request cap is not positive. /// public void Validate() { if (string.IsNullOrWhiteSpace(BearerToken)) { // Names the key, never the value — there is no value here, and there never will be one // in an error message from this type. throw new InvalidOperationException( $"The gRPC secrets hub requires a shared bearer token ({SectionPath}:BearerToken). " + "Until it is set the hub refuses every call."); } if (MaxNamesPerRequest <= 0) { // A zero or negative cap would refuse every GetSecrets call, which reads at runtime like // a broken follower rather than a typo in configuration — so it is refused at startup. throw new InvalidOperationException( $"The gRPC secrets hub request cap ({SectionPath}:MaxNamesPerRequest) must be " + $"greater than zero; it is {MaxNamesPerRequest}."); } } }