Skip to main content

Multi-Tenant Query Boundary

Evidence status: Design-reviewed recipe. The tenant/context methods are application- and model-specific, and no two-tenant Java or Rust integration run is recorded for the current baseline.

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
-> application UserContext resolves tenant
-> generated request applies trusted tenant constraint
-> optional untrusted user filters are merged inside that constraint
-> comment + purpose
-> execute

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

Java Shape

public SmartList<Order> listOrders(
CustomUserContext ctx, String userFilters) {
Merchant tenant = ctx.getMerchant();

return Q.orders()
.filterByMerchant(tenant)
.findWithJsonExpr(userFilters)
.comment("Query tenant orders")
.purpose("Render the current tenant order list")
.executeForList(ctx);
}

The generated tenant filter, context accessor, and dynamic-query terminal order are project-specific. Inspect the generated request API and the selected findWithJsonExpr contract before adapting this shape.

Prefer a reusable application helper when every request for an entity requires the same tenant rule:

static OrderRequest tenantOrders(CustomUserContext ctx) {
return Q.orders().filterByMerchant(ctx.getMerchant());
}

Do not accept merchantId as a replacement for trusted context resolution.

Rust Shape

let tenant = ctx.current_tenant()?;

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

current_tenant() and with_tenant_is() are illustrative. Use the application context and generated request methods that actually exist.

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.

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.Centralize the tenant request helper/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