feat(19.7-01): in-process dispatch marker and compiled handler lookup

- bouncer.Dispatch, WithDispatch, Dispatched: context-only pre-resolved
  principal marker honoured by Registry.Middleware for listed guard names
- cabana.ResolveBackend honours a backend-kind marker; additive
  ResolveBackendToken, ScopeLimitFor and BackendPrincipal resolvers
- surf.Router.Handler and compileHandlers: one compiled chain per route,
  shared by BuildRouter validation and compile
- smoke tests TestDispatchSmoke, TestDispatchedSmoke, TestDispatchMarkerSmoke
- READMEs and docs/services/{authentication,routing}.md updated
This commit is contained in:
Jakub Zych
2026-10-22 16:00:19 +02:00
parent a74a218cd9
commit 1bd91a4522
14 changed files with 522 additions and 11 deletions

View File

@@ -137,6 +137,8 @@ To protect routes, return that middleware from `pact.HasMiddleware` under a name
A guard that resolves more than a user, such as an API token record, implements `bouncer.CredentialGuard`; the middleware stores that record too, and handlers read it with `bouncer.Credential`.
A framework dispatcher that calls a route in-process on behalf of a caller it has already authenticated (the MCP module running a tool through the plugin's own route) does not mint a token for the call. It attaches a `bouncer.Dispatch` marker to the request context with `bouncer.WithDispatch`: the pre-resolved principal, its optional credential and the exact guard names that may honour it. The marker is context-only. Nothing reads a header, cookie or query string for it, so an HTTP request can never carry one, and only a dispatcher inside the framework sets it; plugins never do. `bouncer.Registry.Middleware` honours a marker only for a guard name the marker lists (`bouncer.Dispatched`) and then skips the guard, which covers every guard registered through the registry, such as a plugin's API token guard. `cabana.ResolveBackend` honours a marker for the `backend` guard when its principal is a backend one; a frontend marker is refused as unauthenticated, and a scoped principal is still checked against the cover codes. A marker for one guard has no effect on a route behind another.
### Accepting admin tokens on plugin routes
Plugin routes that should accept an admin JWT or a personal API token call `cabana.ResolveBackend`. It reads only the Authorization Bearer header (never the admin cookie), applies the token's method filter, and for a scoped token requires a method-allowed scope that is full or matches every cover code you pass. On success it returns the request with `bouncer.WithUser` (and `bouncer.WithCredential` for a token). Failures are `cabana.ErrUnauthenticated` or `cabana.ErrTokenScope`. Routes registered before `cabana.Activate` must call it per request, because the resolver is published at activation.