Expand description
Tenant resolution for multi-tenant A2A servers.
TenantResolver extracts a tenant identifier from incoming requests,
enabling per-tenant routing, configuration, and resource isolation.
§Built-in resolvers
| Resolver | Strategy |
|---|---|
HeaderTenantResolver | Reads a configurable HTTP header (default: x-tenant-id) |
BearerTokenTenantResolver | Extracts Authorization: Bearer <token> and optionally maps it |
PathSegmentTenantResolver | Extracts a URL path segment by index |
§Security: all three read what the caller sent
A header, a bearer token and a path segment are all client-controlled.
Nothing in this module authenticates any of them, so a resolver on its own
provides no isolation at all: anyone who can reach the server can send
x-tenant-id: victim and be treated as victim.
RequestHandler::resolve_tenant states the model this is meant to sit
inside — the resolver is the source of truth precisely because it reads
“trusted request context (an auth token, a gateway-set header, a URL path
segment)”. That word trusted is a precondition on the deployment, and it
is the whole of the security argument. It was stated where the value is
consumed and not here, where the resolver is chosen, which is the wrong way
round: nobody picks a resolver by reading the handler.
For that precondition to hold, one of these has to be true:
- a gateway or sidecar in front of this server strips the header from
client traffic and sets it itself from an authenticated identity — the
same discipline
RateLimitConfig::trusted_proxy_hopsapplies toX-Forwarded-For, and for a stronger reason: this decides which tenant’s data you read, not merely whose quota you spend; or - the resolver derives the tenant from something the server has already
verified — a JWT whose signature was checked, for example, via
BearerTokenTenantResolver::with_mapper; or - the deployment has exactly one tenant and this is routing, not isolation.
Two more things worth knowing before relying on this:
- A resolver returning
Nonefalls through to the shared default ("") partition.RequestHandlerBuilder::require_resolved_tenantturns that into a rejection instead, and is off by default for compatibility. - Ordering matters the same way it does for per-caller rate limiting: a resolver that reads something an interceptor is supposed to have established will read nothing if it runs first.
§Example
use a2a_protocol_server::tenant_resolver::HeaderTenantResolver;
use a2a_protocol_server::CallContext;
let resolver = HeaderTenantResolver::default();
let ctx = CallContext::new("message/send")
.with_http_header("x-tenant-id", "acme-corp");
// resolver.resolve(&ctx) would return Some("acme-corp".into())Structs§
- Bearer
Token Tenant Resolver - Extracts a tenant ID from the
Authorization: Bearer <token>header. - Header
Tenant Resolver - Extracts a tenant ID from a configurable HTTP header.
- Path
Segment Tenant Resolver - Extracts a tenant ID from a URL path segment by index.
Traits§
- Tenant
Resolver - Trait for extracting a tenant identifier from incoming requests.