Once a request is authenticated, authorization decides what it may do. This is the
403 half of the AuthN/AuthZ split from lesson one. Two models
dominate, and they answer the question at different granularities.
RBAC: Role-Based Access Control
Permissions are grouped into roles; users are assigned roles. The check asks "does this user's role include this permission?"
const permissions = {
admin: ["read", "write", "delete"],
editor: ["read", "write"],
viewer: ["read"],
};
function can(user, action) {
return permissions[user.role]?.includes(action) ?? false;
}
Simple, easy to audit, and enough for most apps. Its limit is granularity: roles
describe kinds of users, not relationships to specific resources. "An editor can
edit" is easy; "an editor can edit only documents in their own department" forces you to invent
ever-narrower roles (editor-marketing, editor-sales), and they multiply
fast. That's the classic role explosion .
ABAC: Attribute-Based Access Control
ABAC decides from attributes evaluated at request time: attributes of the user, the resource, and the context (time, location, IP):
// "You can edit a doc if you own it, or you manage the team that owns it."
function canEdit(user, doc) {
return user.id === doc.ownerId
|| (user.dept === doc.dept && user.role === "manager");
} This expresses rules RBAC can't reach without exploding into special-case roles: ownership, team membership, "business hours only," "same region." The cost is complexity: policies are harder to enumerate and reason about than a flat list of roles.
Underneath, the two models are answering different questions. RBAC asks "who are you?" once; ABAC asks "what's your relationship to this thing, right now?" on every request. That one difference drives every row:
| RBAC | ABAC | |
|---|---|---|
| What decides? | A role assigned ahead of time | Attributes of user, resource, and context, evaluated live |
| How fine can it cut? | Kinds of users: admins, editors, viewers | This user, this record, this moment |
| "Edit your own docs only"? | Fight it with ever-narrower roles | One comparison: user.id === doc.ownerId |
| Can you audit who has access? | Yes, read the role table | Only by evaluating the policy against every user and resource |
| How does it break down? | Role explosion | Policy sprawl nobody can reason about |
The audit row is the sleeper. "Show me everyone who can delete customer records" is a compliance question you will be asked, and RBAC answers it with a query while ABAC answers it with a research project. That's why the expressiveness of ABAC isn't free even when the policies are simple.
So don't pick one. Use RBAC for the broad strokes, the coarse buckets you want to audit at a glance ("is this an admin?"), and reach for ABAC only where roles genuinely can't express the rule: ownership, team membership, "business hours only." Every ABAC policy you add is auditability you're spending; make sure the rule is worth it.
Node challenge · runs in your browser
RBAC + ABAC sandbox
Estimated time: ~10 minutes
A live sandbox combining both models. Hit Run in Node to see the checks fire, then experiment:
- Add a
managerrole topermissionsand verify it withcan(). - Change
doc.ownerIdto"alice"and re-run: doesbobstill pass? - Add a
canDeletecheck using ABAC: only allow it if the user is an admin or owns the doc. - Try a user with a role not in
permissionsand observe the?? falsefallback.
Check your understanding
Editors should edit only their own team's content: why does RBAC strain?
Your app starts with `admin`/`editor`/`viewer` roles. Then product asks that editors be able to edit only their own team's content. Why does this requirement strain RBAC, and what addresses it?
So far everything, login, credential, roles, has lived inside one app. The next lesson breaks that boundary: what happens when the data a user wants to share lives in someone else's system.