fix(grpc): one public constructor on ControlPlaneAuthInterceptor — two made the gate inert
Caught by the Phase 0 live gate, not by the suite. Grpc.AspNetCore registers the interceptor BY TYPE, and InterceptorRegistration.GetFactory() throws "Multiple constructors accepting all given argument types have been found" when more than one public constructor is applicable. The interceptor had two: the DI one and a prefix-set overload added for later phases. The failure mode is nasty. The throw happens inside the interceptor pipeline on every call, so nothing fails at startup — the site node boots, joins, reports healthy. Every gated call then dies with Unknown / "Exception was thrown by handler", which reads as a handler bug rather than an auth bug. And it fails OPEN in the sense that matters least and closed in the sense that matters most: no call is ever authorized, but no call is ever correctly REFUSED either, so the rig showed identical errors for a correct key, a wrong key and no key at all. Live evidence, site-a: three PullAuditEvents calls, three identical InvalidOperationExceptions in the node log. Fix: the prefix-set constructor is internal (Host.Tests already has InternalsVisibleTo). Later phases extend DefaultGatedPrefixes rather than adding a second public registration shape. Why the tests missed it, and what changed: ControlPlaneAuthEndToEndTests registered the interceptor with AddSingleton alongside AddGrpc, so DI handed back the instance and Grpc.AspNetCore's activation path — the thing that throws — never ran. The harness now registers exactly as Program.cs does, by type and not in DI. Plus a direct reflection assertion that the type has exactly one public constructor, since that is the real invariant and it is cheap to pin.
This commit is contained in:
@@ -205,6 +205,22 @@ public class ControlPlaneAuthInterceptorTests
|
||||
Assert.Equal("ok", await Invoke(interceptor, notGated));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheInterceptorHasExactlyOnePublicConstructor()
|
||||
{
|
||||
// Grpc.AspNetCore registers this interceptor BY TYPE, and
|
||||
// InterceptorRegistration.GetFactory() throws "Multiple constructors accepting all given
|
||||
// argument types have been found" the moment a second public constructor is applicable.
|
||||
// The throw lands inside the pipeline on every call, so the symptom is not a startup
|
||||
// failure but a gate that authorizes nothing and fails everything with
|
||||
// Unknown / "Exception was thrown by handler" — a shape that looks like a handler bug,
|
||||
// not an auth bug. This shipped once and was caught only on the docker rig; the
|
||||
// prefix-set constructor is internal now to keep it from recurring.
|
||||
var publicCtors = typeof(ControlPlaneAuthInterceptor).GetConstructors();
|
||||
|
||||
Assert.Single(publicCtors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultGatedPrefixes_MatchTheRealSiteStreamServicePath()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user