Whetstone.
Multi-Tenant AuthAuthorization, the ladder, and the one exception
Module 5, Lesson 218 min

Authorization, the ladder, and the one exception

This is the densest lesson in the course and it is worth the effort, because authorization mechanics are concrete, testable, and exactly the kind of material a well-built exam question is made of. Three things to learn: the ladder, the return values, and the exception.

The specificity ladder

Narrow to broad, which is also the order the server searches in:

  1. Resource and action: @auth.on.threads.create, or .on("threads:create")
  2. Resource, any action: @auth.on.threads, or .on("threads")
  3. Any resource, one action: .on("*:create")
  4. Global: @auth.on, or .on("*")

Level 3 is the one to look at twice, and it is worth a moment because it is where an intuition breaks. “Any create, on any resource” is a real level, and it sits below resource-specific rather than above it. So if you have registered a handler on threads and another on *:create, and a thread-create arrives, the threads handler wins. Resource beats action. Reaching for “create is the more specific word” gets that backwards.

This is where most people’s instincts misfire, and the instinct comes from somewhere reasonable. Express middleware, Rails filters, most auth stacks anyone has touched: layers stack, every layer can reject, and a request has to survive all of them. Not here. Specificity selects one handler. It does not compose a chain.

The practical consequence is a real footgun and a very fair question. Write a permissive action-level handler underneath a strict global one, and the permissive one wins for that action. Your strict global handler is not a floor. It is a fallback.

The four return values

What a handler returns is what it decides:

Return Effect
None allow
True allow
False deny, 403
a dict metadata filter

Two of the four are simply allow, which is a mercy. False is a hard deny and the status code is 403, exactly the kind of exact value this exam likes to quote at you.

The dict is the interesting one. It is not allow and it is not deny, it is narrow. It scopes which resources the request applies to, which is how you express “this user may list threads, but only their own” without writing that condition into every endpoint by hand. Think of it as a partial allow: yes, you may do this, but only over this slice.

The filter dict supports exact matching and two operators, and the shorthand is the form you will usually see:

Three spellings, first two identical
{"owner": "user-abc123"}                  # exact match, shorthand
{"owner": {"$eq": "user-abc123"}}         # exact match, explicit
{"tags": {"$contains": "premium"}}        # membership

$eq and $contains are the whole operator set. This is not a general query language, and an option offering $in, $gt or $regex is borrowing from one that is.

There is also a fifth outcome worth knowing, even though it is not a decision: returning anything that is not None, a bool or a dict is a 500, not a deny. The system treats a malformed handler as your bug rather than as a rejection, which is the right call and the opposite of failing closed.

The store exception

Now the bait.

Everything above describes resources with metadata you can filter on. The store is not one of those. It is namespaced key-value memory, so there is no metadata field for a filter dict to match against.

This is exam bait in its purest form: a clean general rule with exactly one documented exception. Learn the general rule and stop, and you will pick the metadata-filter option on a store question, and it will feel correct the entire time.

The reason is worth holding, because reasons survive pressure better than facts do. The store already has a scoping mechanism, and it is the namespace. Filtering by metadata would mean inventing a second one on top of a structure that has no metadata to begin with. So authorization uses the mechanism the store already brought with it, which is exactly the point the storage lesson made from the other direction.

The three-question drill

Before answering anything in this focus area, ask in order:

  1. Is this identity or access? That picks authenticate or on.
  2. Which level, and is there a more specific handler registered? That resolves to exactly one handler.
  3. Is the resource the store? That takes one second and catches the single case where everything else you know produces the wrong answer.
Practice

Try it yourself

Quiz

Which handler wins

All three of these are registered. A thread-create request arrives.

Three handlers, three levels
@auth.on
@auth.on.threads
@auth.on.threads.create
  1. AThe global one, because broader handlers act as a gate before narrower ones
  2. B@auth.on.threads, because resource level is the intended middle ground
  3. CAll three run in order from broadest to narrowest, and every one must allow
  4. D@auth.on.threads.create, because the most specific handler wins
Show answer

Correct answer: D — @auth.on.threads.create, because the most specific handler wins

The most specific registered handler wins, and for a thread-create request that is @auth.on.threads.create. The tempting wrong answer is the all-three-must-allow option, because layered middleware where every layer holds a veto is the pattern almost everyone carries in from web frameworks. That is not the resolution rule here: specificity selects one handler rather than composing several, which means a permissive resource-and-action handler registered under a strict global one wins for that action. Note that the ladder has a fourth rung the Python decorator syntax cannot easily spell, any-resource-one-action, and it sits below resource-specific rather than above it.

Quiz

What returning a dict means

Your authorization handler returns a dictionary instead of a boolean. What have you just done?

  1. AApplied the dict as a metadata filter, scoping which resources the caller can see
  2. BRejected the request with a 403 and attached the dict as an error body
  3. CAllowed the request and attached the dict as request metadata for logging
  4. DNothing; only None, True and False are meaningful return values from a handler
Show answer

Correct answer: A — Applied the dict as a metadata filter, scoping which resources the caller can see

A dict return is a metadata filter. It does not simply allow or deny, it narrows the set of resources the request operates over, which is how per-user and per-tenant scoping is expressed without writing that logic into every endpoint. The tempting wrong answer is the logging-metadata one, because attaching a dict of context to a request is what a dict return means in most middleware anyone has written. Here the dict is functional rather than decorative. Note also that False is the value producing a 403, so the rejection option has the right mechanism attached to the wrong return.

Recall

The four return values

Four meaningful returns, and one of them is doing something the other three are not.

What do None, True, False and a dict each mean when returned from an @auth.on handler?

Reveal answer

None allows. True allows. False denies with a 403. A dict acts as a metadata filter, scoping which resources the request applies to rather than allowing or denying outright. So two of the four are plain allow, one is a hard deny with a specific status code worth quoting, and the fourth is the interesting one because it is a partial allow expressed as a filter: yes you may, but only over this slice.

Recall

The store exception

A clean general rule with exactly one documented exception is the purest exam bait there is, so state the exception precisely.

How does authorization scoping work for the store, and why is it different from every other resource?

Reveal answer

The store is scoped by rewriting the namespace rather than by returning a metadata filter. Everywhere else, a dict return narrows results through a filter; for the store, the handler changes the namespace the request operates in. It is different because the store is namespaced key-value memory rather than a queryable collection with metadata attached, so there is nothing for a metadata filter to match against. The namespace is the scoping mechanism the store already has, so authorization uses it instead of inventing a second one.

Check

The three-question drill

Run it on any authorization question before you look at the options. It takes about four seconds.

You should see

You can state the three questions in order and say what each one rules out. First, is this identity or access, which selects authenticate or on. Second, which level applies and is a more specific handler registered, which resolves to exactly one handler rather than to a chain. Third, is the resource the store, which is the one case where the metadata-filter answer you were about to give is wrong and the namespace rewrite is right.

Sign in to track your progress →