Skip to main content

Multi-Tenant Query Boundary

Evidence status: trusted context composition and governance-key rejection exist across all seven runtimes. Automatic Request Policy hook depth differs by language, so portable isolation still requires application tests for every direct-ID, relation, Facet, aggregate, mutation, background-job, and federation path.

Problem​

Return only data belonging to the authenticated tenant, without trusting a tenant identifier supplied by the browser, request JSON, or coding agent.

Applicability​

Use this recipe when tenant ownership is part of the model and application identity is resolved into UserContext.

This recipe does not define the correct tenant/root model for every system. Review model ownership and tenancy rules before implementing the query.

Boundary​

authenticated request
-> trusted server adapter resolves authoritative tenant
-> application initializes UserContext with tenant, actor and policy
-> runtime policy hook and/or trusted provider adapter applies tenant scope
-> business and untrusted filters can only narrow the effective request
-> comment + purpose
-> execute

The client may request sorting, search terms, or pagination. It must not decide the authoritative tenant boundary.

The security invariant must be enforced below ordinary business query construction. An explicit tenant filter in a controller, service, or generated request may be useful business narrowing, but it is not the tenant security boundary: one forgotten filter must not expose another tenant.

Java Shape​

public SmartList<Order> listOrders(
CustomUserContext ctx, String userFilters) {
return Q.orders()
.findWithJsonExpr(userFilters)
.comment("Query tenant orders")
.purpose("Render the current tenant order list")
.executeForList(ctx);
}

Install a mandatory policy while assembling the trusted context. Exact runtime registration APIs are version-specific; this is the intended responsibility:

public final class TenantRequestPolicy implements RequestPolicy {
@Override
public void enforceSelect(UserContext ctx, SearchRequest<?> request) {
appendTenantConstraint(request, ((CustomUserContext) ctx).getMerchant());
}
}

The same policy family must cover mutations and every protected execution shape. Do not accept merchantId as a replacement for trusted context resolution.

Rust Shape​

let orders = Q::orders()
.comment("Query tenant orders")
.purpose("Render the current tenant order list")
.execute_for_list(&ctx)
.await?;

The Rust RequestPolicy reads the authoritative tenant from UserContext and combines its predicate with the request before provider execution. A missing tenant or policy registration must fail closed. Ordinary TFP/federation and dynamic JSON payloads cannot replace that local context state.

Current Hook Coverage​

RuntimeRequest Policy coverageRequired qualification
JavaSelect, insert, update, delete, recoverTest every operation plus nested and analytic reads.
RustSelect, insert, update, delete, recover; entity behavior hooksTest policy and behavior composition.
GoSelect, insert, update, delete, recoverTest every operation and readiness/provider reachability.
SwiftQueryAdd mutation guards at the trusted application/provider/TFP boundary.
PythonQueryAdd mutation guards at the trusted application/provider/TFP boundary.
.NETQueryAdd mutation guards at the trusted application/provider/TFP boundary.
TypeScriptComposition stores requestPolicy; core prepareQuery() does not automatically apply itEnforce Query and Mutation scope in the server adapter/custom data service and retain negative tests.

Do not infer missing application protection from a narrower general hook, and do not claim automatic protection where the runtime does not currently wire it. The composition root remains responsible for complete coverage.

Verification​

Create an integration test with at least two tenants:

  1. Insert one visible record for tenant A.
  2. Insert one record with otherwise identical fields for tenant B.
  3. Query using tenant A's context.
  4. Confirm only tenant A's record is returned.
  5. Submit a user filter containing tenant B's identifier and confirm it cannot broaden the trusted scope.
  6. Repeat for direct ID lookup and association loading.
  7. Repeat for Facets, grouped/relation aggregates, create, update, delete, recover, background jobs, and every enabled TFP endpoint.
  8. Remove tenant, policy, provider, or schema wiring in turn and confirm readiness fails.

Failure Modes​

SymptomRiskFix
Tenant ID comes from request JSONCross-tenant data access.Resolve tenant from authenticated context and apply it as a trusted server constraint.
List query is scoped but ID lookup is notBoundary bypass.Enforce one context-installed runtime policy and test every entry path.
Child relation loads escape tenant scopeCross-tenant association exposure.Review generated relationships and nested request policy.
Background task has no identityUnowned read/write and weak audit trail.Create an explicit service identity and tenant scope in UserContext.

References​