308742b7a6a1d0f009222f643f1cb00a42e71f68
3 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c86cead5c2 |
fix(secrets): rate-limited denial warning on the Grpc hub (live-gate finding) — 0.4.1
The ScadaBridge live gate (check 4) found SecretsHubAuthInterceptor denying correctly but logging nothing: the only trace of a refusal was a framework Grpc.AspNetCore.Server Information line. A follower whose bearer token was mis-rotated therefore stopped converging silently, with the hub showing it only at INF. Every denial is now counted and summarised in a Warning emitted on the first denial after startup or after a 60-second quiet window, carrying the denials since the previous summary plus a breakdown by cause. A single probe is visible immediately; a sustained flood is bounded to one line per window, which is what makes warning safe at all — the endpoint is unauthenticated by construction, so a per-denial warning would be an unbounded log write driven by an unauthenticated caller. Counters are Interlocked and the emit is decided by a compare-exchange on the last-emit timestamp, so exactly one thread emits per window and a denial counted after the drain carries into the next summary rather than being lost. The cause breakdown is server-side diagnostics only. Denied() is untouched: every rejection still returns the identical uniform Unauthenticated status, the comparison is still constant-time, and no token material appears in any log line. The existing warn-once-on-unconfigured-token signal is unchanged and still emitted exactly once; denials in that state count toward the new summary's "unconfigured" category. TimeProvider is an optional default-valued parameter rather than a second overload — the type must keep exactly one public constructor or the gRPC interceptor activator silently stops invoking the auth gate. Two existing tests scoped their assertions to the configuration warning, since both kinds now share a category: the new line is deliberate behavior, not drift. Claude-Session: https://claude.ai/code/session_014WNM4vjoVksyyBraTXSZE1 |
||
|
|
353af72265 |
fix(secrets): Grpc hub server review follow-ups — GetSecrets batch cap, DI single-registration guard + tests
Five review items on the Task 2 server half. 1. GetSecrets now enforces a server-side cap on request.Names, refusing an over-cap batch with ResourceExhausted BEFORE any store read runs. The well-behaved follower chunks at 500, but client discipline is not a server defence — the family precedent (HistorianGateway's max_values) is that the server declines regardless. The new SecretsGrpcHubServerOptions. MaxNamesPerRequest defaults to 1000, comfortably above that chunk so a compliant sweep never meets it, and Validate() refuses a non-positive value at startup rather than letting it read at runtime as a broken follower. The refusal states the cap and the received count — neither is secret, and an operator cannot resize a batch without them — and never a name. The cap sits behind the interceptor, so it is defence in depth against a misbehaving token holder, not an anonymous-DoS control. 2. AddZbSecretsGrpcHub now refuses a second registration. 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 is last-one-wins, so a hub registered twice could serve a token bound from one section while every failure message named the other. Detected via a private marker registration. The extension also gains real DI coverage — the intended single-call path asserted for token/SectionPath coherence through a built provider, a custom section path stamped end to end, a missing token throwing from the registration call itself, and the duplicate call throwing. 3. Client-streaming and duplex-streaming denial tests, so all four gated handler shapes are pinned rather than only unary and server-streaming, plus a valid-token test that reaches all three streaming continuations. 4. A cancellation test: GetSecrets hands the ServerCallContext token through to the store, pinned against a fake that records it. 5. The proto's service comment stated the ISecretReplicationHub mapping in prose that no longer matched the RPC name; it now says GetManifestAsync -> GetManifest and GetManyAsync -> GetSecrets explicitly. Comment-only — no message or rpc definition is touched. The GetSecrets rpc comment also notes the new RESOURCE_EXHAUSTED cap, which a client author has to know about. Grpc server suite 50 -> 73 tests; full solution suite green, 0 new warnings. Claude-Session: https://claude.ai/code/session_014WNM4vjoVksyyBraTXSZE1 |
||
|
|
7fc77f2790 |
feat(secrets): Grpc hub server — SecretsHubService + fail-closed bearer interceptor + DI
The server half of the central-hosted pull-only secrets hub. SecretsHubService adapts the node's own ISecretStore onto secrets_hub.v1 using exactly two read members (GetManifestAsync, GetAsync) — the proto exposes no write RPC, so a follower has no path to put a row into central whatever its credentials allow. GetSecrets omits a name the hub does not hold and skips one that will not parse as a SecretName; neither is an error, because both describe a follower acting on a manifest that was true a moment ago. Failing the call would stall the sweep and let one bad name deny the follower every other row in the batch. Names are re-validated server-side (the wire type is a plain string) and de-duplicated on the normalized value. DomainToWire mirrors the Akka replicator's SecretRowDto discipline exactly: round-trip "O" timestamps so a DateTimeOffset's offset and ticks survive the last-writer-wins equality compare, the content type as its enum MEMBER NAME so an unknown value is rejected rather than decoded to the zero member, and nullable fields left absent rather than written as "" so null and empty stay distinct. SecretsHubAuthInterceptor fails closed: with no token configured every call is denied and one warning names the exact configuration key — never a token value, presented or configured, at any level. Denial is uniform (one Status, one detail, no "why") so a caller gets no oracle. Comparison is constant-time over UTF-8 bytes. All four handler shapes are gated, not just unary, so a future RPC cannot arrive unauthenticated by omission; more than one Authorization header is refused as a smuggling shape. The type keeps EXACTLY ONE public constructor — Grpc's activator silently stops invoking an interceptor it cannot construct unambiguously, and the failure mode is an auth gate that quietly does not run — so a test pins the count. Tests: 50, red-first (compile-red, then a fail-closed mutation verified 5 of them bite). Full Secrets suite 328 passed / 0 failed / 15 live-gated skips, 0 new warnings. Claude-Session: https://claude.ai/code/session_014WNM4vjoVksyyBraTXSZE1 |