Tenant boundaries
Place authentication, object authorization and resource limits at explicit application boundaries.
THE IDEA
Tenant boundaries
In a multi-tenant system every query and every side effect must stay inside the caller's tenant, and that tenant comes from the credential, not from a field the client can edit. Put the tenant filter in the query itself, so another tenant's invoice simply does not exist for you (404). The same separation must hold in cache keys, job payloads and object-store access.
| invoice | tenant | visible to a t1 user? |
|---|---|---|
| 1 | t1 | yes |
| 2 | t1 | yes |
| 3 | t2 | no |
| 4 | t1 | yes |
| 5 | t2 | no |
Predict first. Can changing a tenant ID in a request legitimately switch ownership without a server-side check?
Chapter context
A distributed application must identify callers and control which resources and effects they may access. Tenant boundaries and resource budgets must be enforced by trusted components rather than inferred from client-supplied identifiers.
TRY IT YOURSELF
Part 1. Do it hands-on
Scope data by hand, then write tenant-scoped queries, cache keys and jobs.
| invoice | tenant | visible to a t1 user? |
|---|---|---|
| 1 | t1 | yes |
| 2 | t1 | yes |
| 3 | t2 | no |
| 4 | t1 | yes |
| 5 | t2 | no |
- To do: Five invoices exist: ids 1, 2 and 4 belong to tenant t1; ids 3 and 5 to t2. A correctly scoped list query for a t1 user returns how many?
- To do: A t1 user asks for invoice 3 (t2's). With the tenant filter applied in the query, status code (404 = not found for this tenant)?
- To do: A cache holds 3 resources for each of 2 tenants. Entries needed when the key includes the tenant?
- To do: Can changing a tenant ID in a request legitimately switch ownership without a server-side check? Type yes or no
Part 2. Do it in code
- list_invoices(db, auth_tenant): db is a list of {"id", "tenant"}; only the caller's tenant (from the credential) — never a tenant given in the request.
- get_invoice(db, auth_tenant, invoice_id, requested_tenant=None): the tenant filter is ALWAYS auth_tenant. Return (200, invoice) if it exists in that tenant, else (404, None). requested_tenant is ignored (it is client-controlled input).
- cache_key(tenant, resource): "tenant:resource".
- leaks(cache_keys_used, tenants_seen): the number of distinct keys served to more than one tenant: cache_keys_used is a list of (tenant, key) pairs; count the keys that appear for two or more different tenants.
- job_payload(auth_tenant, task): a job message that carries the tenant FROM THE CREDENTIAL: {"tenant": ..., "task": ...}.
Stuck, or something not working? You can come back to it any time.
CHECK YOUR UNDERSTANDING
Can changing a tenant ID in a request legitimately switch ownership without a server-side check?
Chapter reference: math, worked example & code
This section belongs to the whole chapter, so it is the same on every lesson in it and may cover a different lesson's topic. The code for the experiment you just ran is inside the experiment itself.
Math and notation
For a token bucket with capacity C, refill rate r, previous tokens b and elapsed time dt:
Accept cost k only when , then subtract k atomically. Clock behavior, concurrency and shared enforcement are additional implementation requirements.
Worked example
A bucket holds at most 10 tokens and refills 2/s. An 8-token request at time zero leaves 2. Two seconds later it has 6. A request costing 7 is denied and leaves 6; a request costing 6 is accepted and leaves zero. Rejected operations should not accidentally create negative credit under this contract.
Reference code
The following example uses the Python standard library and does not call a remote service.
capacity,rate,tokens=10,2,10
tokens-=8
assert tokens == 2
tokens=min(capacity,tokens+rate*2)
assert tokens == 6
assert tokens < 7
assert tokens >= 6
tokens-=6
assert tokens == 0
Python examples are provided for study and local execution; this page does not execute Python.
Common mistakes
Authentication is not object-level authorization. Cache and worker paths can cross tenant boundaries too. A global rate limit requires a stated distributed enforcement contract.