CMD Guide
HomeSystem DesignAuthorization

What is Authorization

What is Authorization

Once a system knows who you are, it still has to decide what you are allowed to do. That second decision is authorization. Authentication answers "are you really Alice?"; authorization answers "may Alice delete this invoice?" They are almost always confused in interviews, and getting the distinction crisp is the single fastest way to sound senior.

The problem authorization solves is that not every authenticated user is equal. A logged-in customer, a support agent, and a billing admin all pass the same login, yet they must see wildly different data and buttons. Without a formal authorization layer, every endpoint would hand-roll its own if user.isAdmin checks, permissions would drift, and one forgotten check becomes a breach. Authorization centralizes the rule "subject S may perform action A on resource R" so it is consistent, auditable, and changeable without redeploying business logic.

How it works, precisely

Every authorization decision reduces to evaluating a predicate over three things: a subject (the principal — user, service account, API key), an action (read, write, delete, transfer), and a resource (the specific object, e.g. invoice #4471). A Policy Decision Point (PDP) evaluates the rules and returns PERMIT or DENY; a Policy Enforcement Point (PEP) — code sitting in front of the protected operation — asks the PDP and either lets the call through or returns 403 Forbidden.

Rules come in named models. ACL (Access Control List) attaches a direct list of allowed principals to each resource: "document #4471: alice=read, bob=read+write". It is the simplest mental model and fits single-owner or small-collaborator resources, but becomes unwieldy when many resources share the same membership (every doc in a folder needs its own list). RBAC (Role-Based Access Control) assigns users to roles and roles to permissions: alice → billing_admin → {invoice:read, invoice:delete}. ABAC (Attribute-Based) evaluates conditions over attributes of subject, resource, and environment: "permit if user.dept == resource.dept and time is business hours". ReBAC (Relationship-Based, popularized by Google Zanzibar) answers "is there a path user → owner → document in the relationship graph?" PBAC (Policy-Based Access Control) elevates rules into standalone, versioned policies evaluated by a policy engine (e.g., OPA, AWS IAM policies): "allow read if principal.role == 'viewer' and resource.sensitivity != 'restricted'". It is the most expressive and audit-friendly, but adds a policy language, engine, and deployment pipeline you must operate. Crucially, authorization runs on every request, not just at login — a 403 means authenticated but not permitted, distinct from 401 which means not authenticated at all.

A worked scenario

Imagine a document SaaS like Google Drive at 50,000 QPS of API calls. Every one of those calls must be authorized — an unshared doc must never leak. Two designs:

(A) Call the PDP over the network per request. If each check adds a 2 ms RPC to a central authz service, and that service must sustain 50k QPS, it becomes a hot dependency: any blip there takes down the whole product, and you have added 2 ms to the p50 of every endpoint.

(B) Cache decisions / relationships locally. Google's Zanzibar paper (Pang et al., USENIX ATC 2019) reports serving roughly 10 million authorization QPS at <10 ms p95 globally — with >99.999% availability over three years of production use — using replicated relationship tuples and aggressive caching, with a consistency token ("zookie") to avoid the classic bug of revoking access but a stale cache still permits it. The lesson: at scale, authorization is a read-heavy, latency-critical, correctness-critical subsystem in its own right — often bigger than the feature it guards. A permission check for one doc might expand to "is user in group G, is G shared on folder F, is doc in F?" — a graph traversal you cannot afford to recompute uncached 10M times a second.

Trade-offs: which model, and when

ACL — use when a resource has a tiny, explicit guest list and no inheritance (a personal doc shared with two colleagues). It is the easiest to implement: a table of (resource_id, principal, action). Avoid when permissions must be changed in bulk across many resources or inherited from groups/folders — updating every row is error-prone.

RBAC — use when permissions cluster into a small, stable set of job functions (admin, editor, viewer). It is easy to reason about and audit. Avoid when you hit role explosion: needing billing_admin_region_EU_readonly style combinations means roles are encoding attributes they shouldn't — a smell that you've outgrown RBAC.

ABAC — use when decisions depend on context (department, clearance, time, IP, resource sensitivity). It expresses fine-grained rules without a role per combination. Avoid when policies grow so numerous that no one can predict the outcome of a given request — ABAC trades role-explosion for policy-explosion and is harder to audit ("who can access X?" becomes a search problem).

ReBAC / Zanzibar — use when permissions follow object relationships and inheritance: sharing, folder hierarchies, org charts, "friends of friends." It shines for social and document systems. Avoid when your rules aren't fundamentally relational — you'd pay for a graph engine you don't need.

PBAC — use when you need versioned, centrally authored rules that span services and must be reasoned about as first-class artifacts (OPA, AWS IAM). It gives a single policy language and audit log. Avoid when the team is not ready to operate a policy engine, debug rule evaluation, and version policy changes independently of service deploys.

ModelBest forKey liability
ACLTiny per-resource guest listsBulk changes & inheritance are painful
RBACStable job functionsRole explosion when roles encode attributes
ABACContext/attribute-driven rulesPolicy explosion and audit difficulty
ReBACSharing, ownership, folder inheritanceGraph-engine operational cost
PBACCross-service, versioned policiesPolicy-language/engine operational cost

The orthogonal axis is centralized vs. embedded enforcement. A central PDP (own service, or a sidecar like OPA) gives one source of truth and uniform audit, at the cost of a network hop and a shared failure domain. Embedding checks in each service is fast but risks drift and duplicated, inconsistent logic. Most mature systems land on a central policy definition with a locally-cached/embedded decision engine — policy authored once, evaluated near the request.

Pitfalls an interviewer probes

Key takeaways

Worked config: OPA as PDP behind an nginx PEP

Here is the page's PDP/PEP vocabulary as running config. OPA is the PDP: it evaluates a Rego policy over an input document and returns a decision. The gateway route is the PEP: it must turn that decision into "the request proceeds" or "the request dies with 401/403." The policy allows admins everywhere, and anyone on GET /public:

# policy.rego  (the PDP's rules)
package app.authz
import future.keywords.if
import future.keywords.in

allow if {
    input.user.role == "admin"
}

allow if {
    input.request.method == "GET"
    input.request.path == "/public"
}

The tempting wiring is broken — and silently fail-open. Pointing nginx's auth_request straight at OPA's Data API (proxy_pass http://opa:8181/v1/data/app/authz/allow;) allows every request, for two reasons. First, auth_request decides purely on the subrequest's HTTP status: any 2xx allows, only 401/403 deny. Second, OPA's Data API returns HTTP 200 for both outcomes{"result": true} on allow, {"result": false} (or {} when undefined) on deny — so a deny can never reach nginx as a deniable status. Compounding it, the bare GET subrequest carries no input document at all, so input.user.role is always undefined. This is exactly the fail-open PEP from the pitfalls above, produced by an innocent-looking config.

The working shape puts a thin translator between the two that builds the input document and maps result to a status code — only an explicit true allows, so the PEP fails closed:

# nginx.conf
js_import authz from conf.d/authz.js;

location /api/orders {
    auth_request /_opa;
    proxy_pass http://orders;
}
location = /_opa {
    internal;
    js_content authz.check;
}
// conf.d/authz.js  (the PEP→PDP translator)
async function check(r) {
    // $request_method / $request_uri resolve from the MAIN request,
    // so the subrequest can describe the real call to OPA.
    let res = await ngx.fetch('http://opa:8181/v1/data/app/authz/allow', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ input: {
            // role must come from a VERIFIED JWT the gateway checked at authN —
            // never from a header the client can set itself
            user:    { role: r.headersIn['X-User-Role'] },
            request: { method: r.variables.request_method,
                       path:   r.variables.request_uri }
        }})
    });
    let body = await res.json().catch(() => ({}));
    r.return(body.result === true ? 204 : 403);   // anything but explicit true = deny
}
export default { check };

(Envoy users get this translation for free: the ext_authz filter with the OPA plugin natively maps deny decisions to deny statuses.) The one-sentence lesson worth repeating in an interview: a PEP that infers "allow" from "the PDP answered successfully" is fail-open by construction — the transport status and the decision are different channels, and the PEP must read the decision.

🎯 Drill Ladder — survive the follow-ups

L0 · Authorization is a predicate PERMIT/DENY(subject, action, resource), re-evaluated on every request — the model is easy; surviving the follow-ups on it is not.

L1 · ① Concurrency — "Alice's role is revoked at t=0; her in-flight delete, holding a permission decision cached at t=-200ms, commits at t=+50ms. Did it go through?"
Trap: "The PDP will catch it on the next request." — there is no next request, the race is inside this one; blind caching of PERMIT doesn't know a write happened concurrently.
Bar: pin every read to a version, not to "last known good" — Zanzibar's consistency token ("zookie") stamps a logical timestamp on the revoke-write, and any check must read at-least-that-version, so a racing delete either sees the revoke or the revoke's write is ordered after it, but never silently invisible. connects-to Zanzibar/revocation deep-dive

L2 · ② Failure — "The central PDP is unreachable for 90 seconds. What does every in-flight API call do?"
Trap: "Fail open — a security gap is smaller than a full outage." — that reasoning inverts blast radius: an outage is bounded and visible, an open-authz window is silent and unbounded until someone notices.
Bar: the PEP treats PDP timeout/error as DENY by default (fail-closed); the only sanctioned relaxation is a seconds-scale local cache of prior PERMITs for low-risk, read-only actions behind a circuit breaker that surfaces 503, never a silent PERMIT once that cache is exhausted. connects-to defense-in-depth

L3 · ③ Scale — "PDP is a network hop per call; you're at 50k QPS and it just doubled p50 across the fleet. Fix it without another incident."
Trap: "Horizontally scale the PDP — add replicas." — that fixes throughput but not the per-call RPC tax or the shared-failure-domain coupling; it's the same design, just bigger.
Bar: move the decision in-process — distribute a signed policy/relationship snapshot (or claims-carrying token) so most checks evaluate locally, demoting the PDP to async policy/relationship distribution instead of a synchronous hop, and bound the resulting staleness with the same version token used for revocation. connects-to session-vs-token trade-off

L4 · ④ Time/Lifecycle — "An employee is terminated at 9:00am; their JWT is valid until 5:00pm. Are they authorized at noon?"
Trap: "Short-lived JWTs solve this — drop expiry to 5 minutes." — that shrinks the window, it doesn't close it, and it does nothing for a revocation that happens mid-token-life, which is the actual scenario.
Bar: revocation has to be pushed, not just waited out — a revocation list/blocklist checked on high-value actions, or an event-driven invalidation where the role-change publishes directly to the decision cache, so PERMITs issued before the event are torn down rather than left to expire on their own schedule. connects-to token-revocation-and-refresh-rotation

L5 · ⑤ Adversary/Edge — "A support agent's Tenant-A-scoped service token calls a downstream billing service that forwards the request; can the chain reach Tenant B's invoice #4471 by editing an ID in the payload?"
Trap: "We already checked the caller's role is support_agent at the gateway." — that's a route/role check, not an object-instance check, and it lets the downstream service inherit the gateway's elevated trust instead of the original user's scoped identity: BOLA/IDOR compounded with confused-deputy.
Bar: propagate the original subject's identity through every hop (the downstream call carries the user's token/claims, not the service's own elevated one), and make tenant_id part of the resource key in the policy check itself so a cross-tenant ID simply fails to resolve, not merely fails a permission bit. connects-to authn-vs-authz (401 vs 403, subject identity)

The floor keeps dropping: now do all of the above where the policy language itself is attacker-influenced (an ABAC rule keyed on resource.created_by that the attacker can set), across multiple regions where a revocation write must fence out every replica before it's "done," and justify — for a 20-person B2B tool with three roles — why you are or aren't paying for a Zanzibar-style graph store over an RBAC table in Postgres (⑥ Cost/Simplicity: the sophisticated answer is often "don't build this yet").

Self-locate: died at L1 → mid-level; L4+ → staff signal.

Facing any new concept? Hit it with the six: concurrent? failing? at 100×? over time? adversarial? worth the cost? — that's the interviewer's whole playbook.

🤖 Don't fully get this? Learn it with Claude

Stuck on What is Authorization? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **What is Authorization** (System Design) and want to truly understand it. Explain What is Authorization from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **What is Authorization** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **What is Authorization** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **What is Authorization** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes