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 rolePermissions = {
admin: ["read", "write", "delete"],
editor: ["read", "write"],
viewer: ["read"],
};
function roleAllows(user, action) {
return rolePermissions[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 canEditDocument(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.
Authorization challenge · runs in your browser
Wire up the permission policy
A real HTTP server and a demo document app run in your browser. The server is done except for two blank
functions in permissions.js: the RBAC check and the full policy that layers ABAC on top.
- Implement
roleAllows(user, action). Returntrueonly when the user's role inrolePermissionsincludes the action; fall back tofalsefor unknown roles. - Implement
canPerform(user, action, doc). Admins may do anything. If the role allows the action,readis always fine butwrite/deleteare scoped to ownership or the same department. If the role does not allow it, the doc's owner may still read or write their own doc. Otherwise deny.
Start the server, then click Run permission checks in the demo app. When every row matches its expected outcome, the challenge passes. Edit the code and restart the server to try again.
permissions.ts
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.