Part of #118.
Problem
The proxy's guards (rate limits, JWT, ext_authz, the auth decider, maintenance) cover only the requests its own routes serve. Native gRPC and the fallback pass them all, so an embedder cannot rate-limit or authenticate its gRPC API through the proxy, and cannot choose which traffic a guard covers at all. When a guard rejects, it answers in whatever shape it was written with: JSON {"error","message"} from JWT and the rate limiter, plain text from maintenance, the decider's own body. A gRPC client reading that gets a protocol error instead of a status.
Solution
- Scope per guard. Each guard takes a
scope: which traffic it covers (transcoded, endpoints, grpc, fallback), optionally narrowed by path globs and methods. Traffic classes are structural: a guard is mounted only on the classes it covers, so a request pays nothing for a guard outside its scope; a path or method narrowing is one match inside the guard.
shield.scope, auth.scope (JWT), auth.authz.scope, maintenance.scope, and ProxyServer::with_auth_decider_scope for the injected decider.
- Defaults keep today's coverage: JWT, rate limits and maintenance on
transcoded + endpoints, ext_authz and the decider on transcoded. The forward-auth /verify endpoint is never behind JWT, since it answers that gate.
- Concurrency limit guard.
concurrency: { max_in_flight, scope } sheds requests past the limit at once instead of queueing them; a request holds its slot until its response body ends, so streams count for their whole life.
- Rejections in the request's protocol. Every guard rejects through one path that knows the gRPC code: a REST client gets the
google.rpc.Status JSON body the transcoder already uses (error, code, message, details) with the mapped HTTP status, a gRPC or gRPC-Web client gets a trailers-only response with that code (UNAUTHENTICATED, PERMISSION_DENIED, RESOURCE_EXHAUSTED, UNAVAILABLE) and the guard's headers (Retry-After, RateLimit-*, WWW-Authenticate, Location) as metadata. A decider's HTTP status maps to its code by the google.rpc.Code HTTP mapping.
- Build-time checks. A scope that covers no traffic, an invalid path glob or an unknown method fails when the proxy is built.
Acceptance criteria
Part of #118.
Problem
The proxy's guards (rate limits, JWT, ext_authz, the auth decider, maintenance) cover only the requests its own routes serve. Native gRPC and the fallback pass them all, so an embedder cannot rate-limit or authenticate its gRPC API through the proxy, and cannot choose which traffic a guard covers at all. When a guard rejects, it answers in whatever shape it was written with: JSON
{"error","message"}from JWT and the rate limiter, plain text from maintenance, the decider's own body. A gRPC client reading that gets a protocol error instead of a status.Solution
scope: which traffic it covers (transcoded,endpoints,grpc,fallback), optionally narrowed by path globs and methods. Traffic classes are structural: a guard is mounted only on the classes it covers, so a request pays nothing for a guard outside its scope; a path or method narrowing is one match inside the guard.shield.scope,auth.scope(JWT),auth.authz.scope,maintenance.scope, andProxyServer::with_auth_decider_scopefor the injected decider.transcoded+endpoints, ext_authz and the decider ontranscoded. The forward-auth/verifyendpoint is never behind JWT, since it answers that gate.concurrency: { max_in_flight, scope }sheds requests past the limit at once instead of queueing them; a request holds its slot until its response body ends, so streams count for their whole life.google.rpc.StatusJSON body the transcoder already uses (error,code,message,details) with the mapped HTTP status, a gRPC or gRPC-Web client gets a trailers-only response with that code (UNAUTHENTICATED,PERMISSION_DENIED,RESOURCE_EXHAUSTED,UNAVAILABLE) and the guard's headers (Retry-After,RateLimit-*,WWW-Authenticate,Location) as metadata. A decider's HTTP status maps to its code by thegoogle.rpc.CodeHTTP mapping.Acceptance criteria
google.rpc.StatusJSON body and a gRPC / gRPC-Web client as a trailers-only status with the same code.