Authorisation Models
Date: 2026-08-17
How “may this user do this to this object” gets answered, and where the answer is computed. The model choice matters less than the placement — a perfect policy checked in the wrong layer is bypassed by anyone who calls the layer beneath it.
Authorisation is deciding whether an identified user may perform a given action on a given resource; an authorisation model is the scheme the rules for that are written in.
The models
| RBAC — role-based | ABAC — attribute-based | ReBAC — relationship-based | |
|---|---|---|---|
| Decides from | The user’s role | Attributes of user, object and context | The graph between user and object |
| Rule looks like | admin may delete orders | region == user.region && amount < user.limit | user is editor of a document in a folder they own |
| Fits | Staff tools, admin panels | Regulated, contextual, per-tenant limits | Sharing, hierarchies, collaboration |
| Fails as | Role explosion — editor_uk_readonly_promo | Rules nobody can reason about or test | A graph query on the hot path |
| Answers “who can see X?” | Easily | Badly — you must evaluate every user | Easily, by construction |
Start with RBAC. It covers most systems and everyone understands it. Add attribute conditions to roles as needed — that hybrid is where the majority of real systems land, and it’s a deliberate destination rather than a compromise.
The signal that you’ve outgrown roles is roles being created per customer, per region or per permutation. manager plus a condition on region is one role; manager_uk, manager_de, manager_fr_readonly is the same thing expressed as combinatorics.
Where the check belongs
This is where breaches happen, and the answer is counterintuitive: not in the UI, and not only at the gateway.
UI hides buttons.
NOT a control. the API is a URL and curl exists
↓
API gateway coarse: is there a valid token, is this route allowed
✓ good for authentication and route-level rules
✗ cannot know that order 1234 belongs to this customer
↓
SERVICE / DOMAIN ← the check that matters
knows the user, the object, and the operation
↓
DATA ACCESS the backstop: scope every query by tenant/owner
✓ makes the whole class of bug structurally impossible
The gateway cannot do object-level authorisation because it doesn’t know who owns the object without fetching it — and by the time something fetched it, you’re in the service layer anyway.
The failure this prevents has a name: insecure direct object reference (IDOR) — the endpoint checks you’re logged in, doesn’t check the record is yours, and changing /orders/1234 to /orders/1235 returns someone else’s order. It is consistently among the most common serious web vulnerabilities found in the wild, precisely because the authentication looks fine — Common Vulnerabilities.
✗ const order = await db.orders.find(req.params.id);
return order; // logged in ≠ authorised
✓ const order = await db.orders.find(req.params.id);
if (order.customerId !== session.customerId) return res.status(404).end();
return order;
✓✓ const order = await db.orders.findForCustomer(
req.params.id, session.customerId); // unscoped query impossible
Return 404, not 403, for objects the user may not see. A 403 confirms the record exists, which leaks the existence of accounts, orders and customers to anyone enumerating IDs.
The third form is the one worth building towards: a data layer where you cannot express an unscoped query. Discipline fails at some point across hundreds of endpoints; construction doesn’t — Multi-Tenancy.
Making it hard to get wrong
- Deny by default. New endpoints are inaccessible until a rule permits them. If adding an endpoint without a permission makes it public, every forgotten annotation is a vulnerability
- One place computes decisions. A policy module or engine that every service calls — not a permission check written inline in each controller with slightly different logic
- Permissions are verbs on objects, roles are bundles of them.
order.refundis the permission;support_agentis a bag of permissions. Code checks permissions, never roles — then changing who can refund is configuration, not a deploy - Model the whole operation, not the endpoint. “May refund” and “may refund over £500” are different permissions, and the second is where the money is
- Test authorisation explicitly. For every endpoint: the owner succeeds, a different customer gets 404, an unauthenticated caller gets 401. This is boring, generatable, and catches the entire IDOR class
- Log every denial. A user hitting many denials is either broken tooling or someone probing — both worth knowing — Observability
The parts people forget
- Tenant is the outermost check and is separate from what the user may do inside their tenant — Multi-Tenancy
- Background jobs and internal service calls need an authorisation context too. A job running “as system” with unlimited rights is where privilege escalation hides
- Bulk endpoints, exports and reports frequently skip the per-object check that the single-record endpoint performs. Exports are the classic gap and the most damaging one
- GraphQL needs field- and node-level checks, because one query traverses to objects no single resolver authorised — REST GraphQL and RPC
- Caches must key on the viewer wherever the response varies by permission, or one user’s authorised response is served to another — Caching Strategies, CDN Caching
- Revocation lag. With stateless tokens carrying claims, a demoted user keeps their old permissions until expiry. Either keep tokens short or check critical permissions live — Authentication Architecture
Where it interacts
- Authentication vs Authorisation — the distinction, and why this half is where breaches concentrate
- Authentication Architecture — establishes the identity all of this operates on
- API Design — 401 versus 403 versus 404 is an authorisation decision expressed as a status code