skip to content

Tenant Isolation

Every query in a SaaS backend carries an implicit 'and only this tenant'. Interviewers probe where the tenant is derived and what happens on a worker thread that never received it.

on this pageshow

questions

3

A booking service serves many charterers — where does the server get the charterer identifier for a request, and why never from the request body?

level: juniorimportance: must knowfreq 70%

answer

  1. provenance, not the filter
  2. could the caller have written it
  3. credential claim, host, edge-set header
  4. body value is compared, never trusted
  5. unresolved is a refusal, not all

basics

~20 s

The charterer identifier must come from something the caller cannot rewrite: a claim in the verified credential, the hostname the request arrived on, or a header the edge sets and strips. A body field is caller-controlled input.

solid answer

~40 s

Derive the charterer from a source whose integrity the server has already established. In order of preference that is a claim inside the credential you verified for this request, the hostname the request arrived on when the edge validates it against a registered set, or a header that your own edge writes after deriving it and strips from anything inbound. A charterer id in the JSON body, a query parameter or a path segment is something the caller typed; it may be *compared* against the derived value and rejected on mismatch, but it may never *be* the value. The comparison failure is a `403`, because identity is known and the request is asking about somebody else's data. Bind the derived value once, then let every layer read it rather than accept it.

code

pseudocode · 20 lines
pseudocode
function deriveCharterer(request, identity):
    # fixed order, no silent fallback
    if identity.claims has "charterer":
        derived = identity.claims["charterer"]
    else if request.host is in registeredCharererHosts:
        derived = registeredCharererHosts[request.host]
    else if request.headers has EDGE_CHARTERER_HEADER:
        # the edge strips this header from every inbound request
        derived = request.headers[EDGE_CHARTERER_HEADER]
    else:
        refuse(403, "charterer could not be established")

    if not identity.isMemberOf(derived):
        refuse(403, "identity is not a member of that charterer")

    # the body may disagree; it never wins
    if request.body has "chartererId" and request.body["chartererId"] != derived:
        refuse(403, "body names a different charterer")

    return derived

go deeper

for a junior

Remember the one-line rule: the charterer comes from something the caller cannot rewrite. Be able to name the three trustworthy sources and say why a field in the JSON body is not one of them.

for a middle

Explain the mechanics: which sources have had their integrity established before your handler runs, why a path segment may be compared but never trusted, and why an unresolved charterer must refuse rather than fall through to an unfiltered query.

for a senior

Show the production judgment. Talk about the claim that went stale after a membership change, the edge header that stopped being trustworthy when the origin became directly reachable, and the logging-only body field that quietly became a cache key.

for a principal

Own the provenance as a system property. One derivation function, one documented order, one refusal path, and a rule that nothing caller-supplied enters the request context — so a reviewer answers where did this come from once for the whole service instead of at every call site.

A multi-charterer booking platform has one invariant that outranks every other rule in it: every read and every write carries an implicit *and only this charterer*. The `WHERE` clause that expresses it is the boring half. The interesting half is where the value in that clause came from, because a filter applied to an attacker-supplied constant filters nothing. ## Trust is a property of the source, not of the field name A request arrives with many fields that look like they name a charterer. Only some of them have had their integrity established before your handler runs. The question to ask of each candidate is: **could the caller have written this, and would the server have noticed?** | Source | What makes it trustworthy | What it costs you | |---|---|---| | A claim inside the verified credential | The signature or the server-side session lookup was checked before the handler ran; the value was frozen by whoever issued the credential | The value is only as fresh as the credential — a charterer membership revoked five minutes ago is still asserted until it expires or is re-checked | | The hostname the request arrived on | The edge terminates the connection for a certificate you control and matches the host against a registered set | A caller who can reach the origin directly, bypassing the edge, can set any host they like | | A header your own edge writes | The edge derives it and **strips the same header from every inbound request**, so it cannot be forged | Worthless the moment the origin is reachable without passing the edge, or the strip rule is forgotten on one route | | A field in the body, the query string or the path | Nothing | It is caller input wearing a plausible name | ## Why the body is disqualified even when it looks harmless The obvious failure is direct: accept `chartererId` from the body, use it in the filter, and any authenticated user reads any charterer's bookings by changing one number. The version that actually ships is subtler. Someone adds the body field *only for logging* — a breadcrumb so support can see what the client thought it was doing. Six months later that same variable is convenient, so it becomes part of a cache key, or a metric label, or the argument to a reporting query. Nobody re-audits its provenance, because by then it is just a variable. **A caller-controlled value that exists in the request-handling code at all will eventually be used as if it were derived.** The defence is not discipline; it is never letting it into the context object in the first place. A path segment such as `/charterers/CHR-4417/bookings` is a *request about* a charterer, not a *statement of* the request's charterer. Compare it against the derived value and refuse on mismatch. That refusal is a `403`: the server knows who the caller is and is declining. A `401` with `WWW-Authenticate` means something different — *I do not know who you are* — and using it here tells the client to re-authenticate, which will not help and hides a real access violation inside a routine login loop. ## The order the server actually runs 1. **Authenticate.** Establish the caller's identity from the credential. No identity, no charterer: `401` with `WWW-Authenticate`. 2. **Derive.** Read the charterer from the credential's claim, or from the validated hostname, or from the edge-written header — in a fixed, documented order, with no silent fallback. 3. **Verify membership.** If the identity and the charterer come from different sources, confirm the identity is actually a member of that charterer. Two trustworthy values can still disagree. 4. **Bind.** Put the single resolved value where the rest of the request can read it, and nowhere else. 5. **Fail closed.** If nothing resolves, refuse. On an authenticated, charterer-scoped endpoint that is a `403` — not an empty filter, not a default charterer, and not the first one in the list. Step 5 is the one people skip. An unresolved charterer is a missing constraint, and a missing constraint in a filter is not *no rows*; in most data-access layers it is *all rows*. The absent case and the all case must never be the same value in your code. ## What this buys the reviewer Once derivation is one function with one documented order and one refusal path, `where did this charterer come from?` has exactly one answer for the whole service, and a reviewer can check it once rather than at every call site. That is the property you are actually designing for — not the filter, which everyone remembers, but the provenance of the thing the filter compares against, which nobody does.

  • The credential's charterer claim was frozen when the credential was issued. A user was removed from that charterer ten minutes ago. What does your service serve?
    It serves that charterer's data until the credential expires or you re-check membership. A claim is a snapshot of the issuer's belief at mint time, not a live lookup. Either keep credential lifetimes short enough that the window is acceptable, or re-verify membership from your own store on each request and accept the lookup cost. Say which trade you took; both are defensible, silence is not.
  • Your edge derives the charterer and forwards it in a header. What single deployment mistake makes that header worthless?
    Leaving the origin reachable without passing the edge, or forgetting to strip the same header from inbound requests on one route. Either way any caller can set it themselves and pick their charterer. A header is only as trustworthy as the guarantee that nothing but your edge can write it, and that guarantee is a network and configuration property, not a code property.
  • A support tool needs to read one charterer's bookings while signed in as a support engineer who belongs to no charterer. Does that break the rule?
    No, but it is a different path and must be built as one. The support request is explicitly crossing charterers, so it needs its own deliberate mechanism with its own record of who did it and for whom — not a relaxation of derivation on the ordinary path. The moment `derive the charterer` grows an `unless the caller is staff` branch, every ordinary request inherits that branch.

A port gate checks the pass you hand over against its own register. It will happily read the company name you wrote on the delivery note, and it will compare the two — but the note never becomes the answer.

saying these in an interview costs you the question

  • Takes the charterer id from the request body because the client always sends the right one
  • Believes a path segment naming a charterer is a statement of the request's charterer
  • Treats a hostname as trustworthy even when any caller can set the host the server reads
  • Answers `401` when a caller asks for another charterer's data
  • Lets an unresolved charterer fall through to no filter, which returns every row
  • Thinks a body field is safe because it is only written to logs
open as a page

Your booking service derives the charterer at the edge — how do you make that value reach every layer, including cache keys and log fields?

level: middleimportance: must knowfreq 58%

basics

~10 s

Bind the derived charterer once to a request-scoped context any layer can read without being passed it, immutable for the request, cleared when it ends, and required in every cache key and log field.

open as a page

A scheduled job and a retried queue message run with no charterer bound in the request context — what must happen, and why is 'all charterers' the dangerous default?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Unbound must mean refuse, not proceed. A data-access layer that drops the charterer predicate when nothing is bound returns every charterer's rows, so make the read raise; background work binds a charterer explicitly, per message, and clears it afterwards.

open as a page