How software gets permission today
A hotel, a valet key and one very long answer to "can I do this?"
Before I start arguing that something is missing, I should probably show what is already there.
This is that note. Identity, authentication, authorization, RBAC and friends, OAuth, tokens, scopes, service accounts, policy engines. The whole pile.
It is long. Sorry. I tried splitting it, and every piece kept leaning on the others.
How to read this
People reading this sit anywhere between "what is authorization?" and "I have opinions about Zanzibar consistency tokens". So most ideas get explained more than once, from a different angle each time:
- The hotel. One analogy for the whole note. Easy paragraphs, skip them if you know the concept.
- The software. The same idea, in one made-up but very ordinary system.
- Deeper cut boxes. Details, edge cases and a few opinions for people who do this for a living. Skip them if you are new, nothing later depends on them.
Skim the first two sections, have a look at Many ways to get it wrong, and then jump to Known actors, known requests. That's where this series actually starts going somewhere.
The cast
The hotel. You are checking in. There is a receptionist, a key card, rooms, a pool, a spa, housekeeping, a valet and a laundry company that shows up every morning.
The software. Acme is a company with an internal purchasing app. People request things, managers approve them, finance pays.
- Anna is an engineer. She requests a monitor.
- Ben is her team lead. He approves orders for his team.
- Acme Purchasing is the app, with an API behind it.
- ExpenseBuddy is a third-party app Anna wants to connect to her orders.
- invoice-sync is a nightly job which pushes approved orders to accounting.
That's everyone. They will keep coming back.
Who are you, prove it, what can you do
These three questions are the whole field in one line. They have names: identity, authentication and authorization. Put together (plus all the admin work of creating, changing and removing access) they are usually called IAM, Identity and Access Management.
They also always happen in this order, and each one only makes sense on top of the previous one. So let's go in order.
Identity: who are you?
The hotel. You walk up to the desk and say "Hi, I'm Dmitrijs, I have a booking."
Notice what happened. You didn't create your identity at the desk. The hotel already has a guest record: a name, a booking, a loyalty number, maybe a note saying you asked for a quiet room last time. You are just pointing at that record.
That record is the identity. Your name is just how you point at it.
The software. Anna has a user record in Acme's system. It has an ID like usr_8f3a, an
email, a department, a manager. When Anna types her email into a login form, she is doing the
same thing as you at the desk: claiming to be a specific record.
The claim alone proves nothing. Anyone can walk up and say "I'm Dmitrijs".
An email is an identifier, not the identity. Emails change, get recycled, and domains expire and get re-registered (a real, repeatedly exploited way to take over accounts). Good systems key everything on a stable, never-reassigned ID and treat email as an attribute.
In OpenID Connect this is the sub claim, and it is only unique per issuer, so the actual
key is the pair (iss, sub). Also, the record often doesn't live in the app at all: it lives
in an identity provider (Entra ID, Okta, Google Workspace…), and the app just trusts
it. More on that in a second.
Authentication: prove it
The hotel. The receptionist asks for your passport. Photo matches your face, name matches the booking. Fine, you are you.
Then comes the important bit. The receptionist gives you a key card.
From now on nobody asks for your passport. Not the elevator, not the room door, not the pool. They check the card. The passport check happened once, and the card is proof that it happened.
The software. Anna logs in: password, passkey, a push notification on her phone, whatever Acme set up. That's authentication. Proving the claim from the previous step.
And then, exactly like in the hotel, the app hands her something to carry around: a session cookie or a token. Every following request carries that instead of her password.
This is why stealing a session or a token is so attractive to attackers. Nobody needs your passport if they can pick up your key card from the bar counter.
The hotel doesn't verify you itself. It trusts that a government checked you and issued a
document it can validate. That's federation, and it's how most corporate login works:
Acme Purchasing never sees Anna's password. Anna authenticates with Acme's identity
provider, which hands the app a signed statement saying "this is usr_8f3a, authenticated
30 seconds ago, with MFA". In OpenID Connect that statement is the ID token. In older
enterprise land it's a SAML assertion.
Authentication factors are traditionally something you know (password), have (phone, security key) or are (fingerprint). Passkeys are the interesting modern one: the credential is bound to the website's origin, so a phishing site simply can't ask for it. Most "MFA" is not phishing resistant. Passkeys actually are.
Authorization: what can you do?
The hotel. Your card opens room 412. It doesn't open 413. It opens the pool, because you are a gold member. It doesn't open the staff kitchen, no matter how nicely you ask.
Every door asks the same question: can this card open me?
And it only works because of the two steps before. If the hotel skipped the passport check, the pool door would be asking "is this person gold?" about someone who just said they were you. Authorization on top of weak authentication is decoration.
The software. Anna clicks "Approve" on an order. The API has to decide whether to let her. Stripped down, every authorization check in every system answers one question:
Can this subject do this action on this resource?
Can usr_8f3a do approve on order:1042? Yes or no.
That's it. Everything else in this note is about how systems answer that question, and who the subject is.
Authentication is an event. Authorization is continuous: it runs on every request, often several times per request. That's why its performance, caching and consistency matter so much more than people expect.
The classic vocabulary (from XACML, still used everywhere) splits the job up: the PEP (policy enforcement point) is the door lock, the PDP (policy decision point) is whatever decides, the PIP (policy information point) is where facts like "is this guest gold?" come from, and the PAP (administration point) is where someone writes the rules.
Fun fact: HTTP 401 Unauthorized actually means unauthenticated. 403 Forbidden is the
real "not authorized". The spec named it wrong in 1996, and we have been explaining it
in code reviews ever since.
One more thing, and it's an opinion. Identity and authentication are mostly solved problems
you can buy. Authorization is not, because the rules are your business. No vendor knows
that at Acme, team leads can approve orders up to €2,000 but not their own. That knowledge
has to live somewhere, and "somewhere" is usually a couple hundred if statements.
| Question | Name | Hotel | Acme | When it fails | |
|---|---|---|---|---|---|
| 1 | Who are you? | Identity | "I have a booking" | usr_8f3a | Wrong person gets the record |
| 2 | Prove it | Authentication | Passport | Passkey, MFA | 401 |
| 3 | What can you do? | Authorization | Card opens 412 and the pool | Anna can request, Ben can approve | 403 |
How the "yes or no" gets decided
So the question is always subject, action, resource. The interesting part is where the answer comes from. There are a few families of models, and each one grew out of the limitations of the previous one.
Lists on the door (ACLs)
The hotel. Imagine the pool door with a paper list taped to it: names of everyone allowed in. Guard checks the list.
Simple. Also a nightmare for a hotel with 400 guests changing daily.
The software. An access control list attaches the "who can do what" directly to each resource. Unix file permissions, S3 bucket policies, "Share with…" in Google Drive. Order 1042 has a list: Anna can view, Ben can approve.
ACLs are great at answering "who can access this thing?" and awful at "what can Anna access?". You'd have to check every list in the building. And when Anna leaves, someone has to find her name on every door.
Roles (RBAC)
The hotel. Instead of names, the hotel thinks in roles: guest, gold guest, housekeeping, front desk, manager. The pool door lets in gold guests. The linen room lets in housekeeping. A new housekeeper gets the housekeeping role and every right door opens on day one.
The software. Role-based access control puts a layer in between people and permissions. Permissions go to roles, roles go to people.
| Permission | Requester | Approver | Finance | |
|---|---|---|---|---|
| 1 | Create order |
|
|
|
| 2 | Approve order |
|
|
|
| 3 | Pay invoice |
|
|
|
| 4 | View all orders |
|
|
|
Anna is a Requester. Ben is a Requester and an Approver. Done.
RBAC is by far the most common model in the wild, and for good reason. It's easy to understand, easy to audit ("who is an Approver?" is one query), and it maps to how companies already think about jobs.
Now the problems. According to the table, Ben can approve any order. From any team. For any amount. Including his own.
You can fight that with more roles: Approver-Platform-Team, Approver-Under-2000,
Approver-Platform-Team-Under-2000… and now you have a role explosion. I have seen
systems with more roles than employees. Not a joke.
The real issue is that a role describes the person, but half the rules depend on the thing: whose order is it, how big is it, which team.
The NIST RBAC model (the one standardized in the early 2000s) has levels: flat roles, role hierarchies (Manager inherits Employee), and constraints like separation of duties ("nobody may be both Requester and Payer"). Most real systems implement only the first level and bolt the rest on in code.
Cloud RBAC (Azure, GCP) quietly fixes part of the problem by attaching role assignments to a scope: "Approver on resource group Platform". That's RBAC plus a resource hierarchy, which is already halfway to the next two models.
Attributes and rules (ABAC)
The hotel. Some rules have nothing to do with who you are. The pool is open 7:00 to 22:00. The spa is 18+. Late checkout is fine for gold guests if the hotel is under 80% occupancy. The door isn't checking a list or a role, it's evaluating a rule over facts: about you, about the room, about the time.
The software. Attribute-based access control decides with rules over attributes of the subject, the action, the resource and the environment. Acme's actual approval rule looks something like this:
1allow approve on order when2 user.role == "approver"3 and order.costCenter == user.costCenter4 and order.amount <= user.approvalLimit5 and order.requester != user.id
One rule. No role explosion. Ben can approve orders from his own cost center, up to his limit, and never his own.
That's the upside: ABAC is very expressive, and it handles amounts, time windows, locations and "not your own thing" naturally.
The downside is the reverse of RBAC. "Can Ben approve order 1042?" is easy. "Who can approve order 1042?" or "what can Ben approve?" means evaluating rules against everyone, or everything. Auditors hate that. And the decision is only as good as the attributes: if Anna's cost center is stale in the HR system, the rule is confidently wrong.
Writing the rule takes ten minutes. Getting user.approvalLimit, order.costCenter and
"is this device managed?" to the decision point, fresh and trustworthy, takes the rest of
the project. That's the PIP from earlier, and it is where most ABAC deployments quietly
struggle.
ABAC's formal ancestor is XACML, an XML policy language that was very thorough and that almost nobody enjoyed writing. The modern tools (more on them below) are basically ABAC with a nicer syntax.
Relationships (ReBAC)
The hotel. Why can you open room 412? Not because of a role. Not really because of a rule. Because you are on the booking for 412. Your partner is on the same booking, so their card opens it too. Your kid's card opens the connecting room, because the booking says so.
Access follows relationships.
The software. Relationship-based access control models the world as a graph and asks whether a path exists. Why can Ben approve Anna's monitor order?
1order:1042 --requested_by--> user:anna2user:anna --member_of-----> team:platform3user:ben --lead_of-------> team:platform
Rule: "a team lead can approve orders requested by members of their team". There's a path, so yes.
If this looks familiar, it's because you use it every day. Google Drive works exactly like this: a document sits in a folder, the folder is shared with a group, Anna is in the group, so Anna can open the document. Nobody assigned Anna a role for that document.
ReBAC is great for anything with ownership, sharing, hierarchy and teams, which is most modern SaaS. It's what RBAC wanted to be when it said "Approver of this team".
Modern ReBAC comes mostly from Google's Zanzibar paper (2019), describing the system
behind permissions in Drive, YouTube, Calendar and others. Relationships are stored as
tuples like team:platform#member@user:anna, and checks are graph traversals.
The genuinely hard part is consistency, the "new enemy problem": you remove Bob from a folder, then add a secret document to it. A stale replica can evaluate the second event without the first, and Bob sees the document. Zanzibar solves it with consistency tokens ("zookies"). OpenFGA, SpiceDB, Permify and others are open implementations, and most now support conditions on relationships (OpenFGA conditions, SpiceDB caveats), which is ABAC sneaking back in.
A few other names you'll run into
| Model | One-liner | Hotel | |
|---|---|---|---|
| 1 | DAC (discretionary) | Owners decide who gets access to their stuff | You give a friend a spare card to your room |
| 2 | MAC (mandatory) | A central authority labels everything, nobody can override | Floor 20 is for diplomats. Period. |
| 3 | PBAC (policy-based) | Mostly a marketing name for ABAC with policies as code | - |
| 4 | Capabilities | Holding an unforgeable token is the permission | The key card itself, whoever holds it |
The last row matters for this series. A physical key doesn't care who holds it. That's a feature when you want to hand authority to someone else, and a disaster when it leaks. It'll come back in the note on delegation.
So which one?
All of them, honestly. I have never seen a real system running a pure model.
The usual shape is roles for the coarse stuff ("is this person allowed in the finance area at all?"), relationships for ownership and sharing ("is this order from your team?"), and attribute conditions for the fiddly bits ("under €2,000, not your own, not on weekends").
They aren't really competitors. They are three different ways of finding the facts needed to answer the same question: subject, action, resource.
Many ways to get it wrong
Here is the thing people outside security tend not to know.
Broken access control is the number one risk in the OWASP Top 10. It took the top spot in 2021 and kept it in the 2025 edition, now covering 40 different weakness types, the most of any category. The OWASP API Security Top 10 is even more blunt: three of its top five risks are authorization bugs.
Not injection. Not crypto. "The app let someone do something they shouldn't have."
And that's with humans clicking buttons and code written by people. No AI involved. Let's look at the usual suspects.
Changing the number in the URL
The hotel. You find out the room door doesn't actually check your card against the room. It checks that it's a valid card. So your card opens 413. And 414.
The software. Anna opens GET /orders/1041 and sees her order. She changes it to
/orders/1042. The API checks that she is logged in, but not that order 1042 is hers.
This is called IDOR (insecure direct object reference), or BOLA (broken object level authorization) in the API world. It's the most boring bug imaginable, and it is everywhere. In 2019 First American Financial exposed around 885 million mortgage-related documents this way: sequential IDs in URLs, no ownership check.
The hidden button
The hotel. The staff door isn't locked. It just says "Staff only".
The software. The "Pay invoice" button only shows up for Finance. But
POST /invoices/77/pay doesn't check the role, so anyone who knows the URL (or opens browser
dev tools) can call it. Hiding UI is not authorization.
The confused deputy
The hotel. The front desk clerk can open every room. You say "I locked myself out of 413", confidently, and they open it. You never had authority over 413. The clerk did, and you borrowed it.
The software. A confused deputy is a program with more authority than its caller, tricked into using it on the caller's behalf. Acme Purchasing has a "fetch product details from URL" feature. Anna gives it an internal URL instead, and the server, which can reach internal systems, happily fetches it for her. That's SSRF, and it's the reason SSRF was folded into broken access control in the 2025 OWASP list. The 2019 Capital One breach started roughly like this: a server tricked into fetching cloud credentials it was allowed to see and the attacker wasn't.
The confused deputy is from 1988, and I think it's about to become the most important idea in this whole series. An agent is, more or less by definition, a deputy which acts on instructions from text it didn't write.
Access that never goes away
The hotel. You checked out yesterday. Your card still opens the room.
The software. Anna moves from Platform to Data. Her old Approver role stays, because removing access is nobody's job and adding it is somebody's urgent ticket. Two years later she has permissions from four teams. This is privilege creep, and the fix (the "joiner-mover-leaver" process) is less a technology than an act of discipline.
"Just give it admin, we'll fix it later"
The hotel. The laundry company gets a master key, because making a special one was annoying.
The software. invoice-sync needs to read approved orders. It gets a service account with full admin rights, because the narrower permission didn't exist yet and the release was on Friday. Nobody fixes it later. Later never comes.
Forty endpoints, thirty-nine checks
The software. When authorization lives in if statements scattered across the codebase,
every new endpoint is a new chance to forget one. And nobody notices, because a missing check
doesn't break anything. It just lets something through.
| Failure | Hotel | Acme | Result | |
|---|---|---|---|---|
| 1 | IDOR / BOLA | Card opens any room | /orders/1042 isn't Anna's |
|
| 2 | Missing function check | "Staff only" sign, no lock | Pay endpoint doesn't check role |
|
| 3 | Confused deputy | Clerk opens 413 for you | Server fetches internal URL |
|
| 4 | Privilege creep | Card works after checkout | Anna keeps old Approver role |
|
| 5 | Over-permissioned service | Laundry gets master key | invoice-sync is admin |
|
Every single one of those should have been a deny.
So why is this so hard?
My take: three reasons.
It's business logic. You can't buy a library that knows who at Acme may approve what.
It's everywhere. Every endpoint, every query, every background job, every export.
And the worst one: it fails silently. A broken feature gets a bug report. A broken
permission check returns 200 OK with a nice JSON body, to the wrong person. Our tests
check that people can do things. Very few check that they can't. Nobody files a ticket
saying "I was able to see something I shouldn't have".
So, before anyone mentions agents: authorization is already a hard, unglamorous, badly tested problem. I think that alone makes it worth writing about.
The actual machinery
Now let's go one level down. Everything so far was mostly about humans in one app. Real systems have programs talking to programs, third-party apps, and tokens flying between them.
Not everyone is a person
The hotel. Not everyone walking the corridors is a guest. There's the laundry company, the elevator maintenance crew, the booking website pushing reservations into the hotel system, the minibar restocking robot. They all need to open doors too.
The software. invoice-sync runs at 2 AM. Nobody is logged in. Whose permissions does it use?
The wrong answers are "Ben's" and "the admin's". The right answer is: its own. That's a service account (also called a machine identity or non-human identity): an identity that belongs to a program instead of a person.
Traditionally, a service account authenticates with a secret: an API key, a client secret, a certificate. Like the laundry guy's badge. It works, but badges can be copied, and secrets end up in config files, CI logs and, occasionally, public GitHub repos.
The modern answer is workload identity. Instead of carrying a secret, the program proves who it is by where and how it runs. The platform vouches for it: "this is the invoice-sync job, running in Acme's Kubernetes cluster, in the finance namespace." In hotel terms, the elevator doesn't need a badge. It's bolted into the building. Everyone knows it's the elevator.
AWS IAM roles for EC2/EKS, GCP service accounts, Azure managed identities, Kubernetes
service account tokens: all variations of "the platform issues short-lived, signed
credentials to the workload". Workload identity federation extends it across
boundaries, the best known example being GitHub Actions getting an OIDC token and swapping
it for AWS credentials, no stored keys. SPIFFE/SPIRE standardizes the whole thing in
a platform-neutral way, with IDs like spiffe://acme.com/finance/invoice-sync.
Machine identities outnumber human ones by a very large factor in most companies (the figures you'll see quoted vary wildly, but it's never close). They also tend to have the broadest permissions and the least oversight. Nobody does quarterly access reviews for a cron job.
OAuth: the valet key
The hotel. You arrive by car. You don't hand the valet your house keys, your passport and your credit card. You hand them the valet key: it starts the engine and opens the doors. It doesn't open the trunk or the glovebox. And it's useless after you pick the car up.
The software. Anna wants ExpenseBuddy, a third-party app, to read her Acme orders and build expense reports.
The old way (and I mean pre-2010, but also some tools today): Anna types her Acme password into ExpenseBuddy. Now ExpenseBuddy can do everything Anna can do, forever, until she changes her password. Which breaks every other integration too.
OAuth 2.0 is a protocol for handing out valet keys instead. It has four parties:
- Resource owner: Anna, whose orders these are.
- Client: ExpenseBuddy, the app asking for access.
- Authorization server: Acme's identity provider, which issues the keys.
- Resource server: Acme Purchasing's API, which accepts the keys.
The flow, simplified:
- ExpenseBuddy sends Anna to Acme's authorization server: "ExpenseBuddy would like to read your orders."
- Anna logs in (authentication, at Acme, not at ExpenseBuddy) and sees a consent screen. She clicks Allow.
- The authorization server sends Anna back to ExpenseBuddy with a one-time code.
- ExpenseBuddy exchanges that code, server-to-server, for an access token.
- ExpenseBuddy calls Acme Purchasing's API with the token.
ExpenseBuddy never sees Anna's password. The token only allows reading orders. Anna can revoke it without changing anything else.
That's delegated authorization: Anna delegates a slice of her authority to an app.
There's a second flow that matters just as much: client credentials. No user involved, the client just authenticates as itself and gets a token. That's how invoice-sync gets a token at 2 AM. Service accounts and OAuth meet here.
The most common OAuth misunderstanding: it's a protocol for authorization, and the access token is meant for the API, not for the client to learn who the user is. "Sign in with Google" is OpenID Connect, a thin identity layer on top of OAuth which adds the ID token. Apps that used plain OAuth access tokens as proof of login have historically been easy to break.
Also worth knowing: OAuth 2.1 consolidates a decade of security lessons (implicit grant gone, PKCE required for everyone, exact redirect URI matching). Token exchange (RFC 8693) lets a service swap one token for another, which is how "on behalf of" chains between services are usually built. That one gets its own note next time.
Access tokens: the key card
The hotel. The key card is the thing that actually opens doors. The door doesn't call the front desk every time. It reads the card and trusts it, because only the front desk can make valid cards.
The software. The access token is the key card. Often it's a JWT: a small JSON document, signed by the authorization server, so the API can verify it without calling anyone. Here's what ExpenseBuddy's token might look like, decoded:
1{2 "iss": "https://login.acme.com",3 "sub": "usr_8f3a",4 "aud": "https://purchasing.acme.com",5 "client_id": "expensebuddy",6 "scope": "orders:read",7 "iat": 1789812000,8 "exp": 17898129009}
iss: who issued it. The API only trusts tokens from Acme's login server.sub: whose authority this is. Anna's.aud: who it's for. Only the Purchasing API should accept it.client_id: who is holding it. ExpenseBuddy, not Anna.scope: what it's for. Reading orders.iat/exp: issued at, expires at. Fifteen minutes here.
The API checks the signature, the issuer, the audience and the expiry. If all good, it knows who (Anna), through what (ExpenseBuddy) and for what (reading orders).
The catch: these are bearer tokens. Like a key card, whoever holds it can use it. No questions asked. That's why they're short-lived, and why long-lived refresh tokens (used to get new access tokens) are guarded much more carefully.
A JWT is valid until it expires. You can't really "un-sign" it, so revocation is hard. The alternatives are opaque tokens plus introspection (RFC 7662), where the API asks the authorization server every time, trading latency for control. Most systems compromise with short lifetimes.
The fix for "whoever holds it can use it" is sender-constrained tokens: DPoP (RFC 9449) or mutual TLS bind the token to a key the client holds, so a stolen token alone is useless. Adoption is growing, but bearer tokens are still the default almost everywhere.
And forgetting to check aud is a classic: a token issued for one API gets replayed
against another one that trusts the same issuer.
Scopes: what the valet key is for
The hotel. The valet key opens doors and starts the engine. Not the trunk. That restriction is the scope.
But here is the part people miss: the valet key can't do anything the car owner couldn't. If you rented the car and the rental contract says "no leaving the country", the valet can't leave the country with it either.
The software. A scope like orders:read limits what the client may do with the
user's authority. It does not grant anything on its own.
So what ExpenseBuddy can actually do is the intersection: what Anna can do, and what the token's scopes allow.
This means the API still has to do its own authorization. orders:read doesn't mean "read all
orders", it means "read orders, as Anna". If the API only checks the scope and not whether
order 1042 is Anna's, we're back to the IDOR from earlier, just with extra steps.
| Request from ExpenseBuddy | Anna can? | Scope allows? | Result | |
|---|---|---|---|---|
| 1 | Read Anna's order 1041 | yes | yes |
|
| 2 | Read Ben's order 1099 | no | yes |
|
| 3 | Create a new order for Anna | yes | no |
|
| 4 | Approve an order | no | no |
|
Scopes also have a granularity problem. They are defined at design time, by the API
developer, and they're usually coarse: orders:read, orders:write. Try to make them
precise and you get orders:read:team:platform:under-2000, which is role explosion wearing a
different hat.
Rich Authorization Requests (RFC 9396) replace the scope string with structured JSON,
authorization_details, which can carry things like "one payment of €640 to this vendor".
It came from open banking, where "payments:write" was obviously not enough. Keep it in mind,
it's the closest thing the OAuth world has to what later notes will be poking at.
Policy engines: one rulebook instead of forty doors
The hotel. In an old hotel, each door had its own logic, wired in when it was installed. Change the pool hours, and someone walks around reprogramming locks. A modern hotel has one central system: the doors ask it, it answers. Change a rule once, every door follows.
The software. Remember the if statements scattered across forty endpoints? A policy
engine pulls the decision out of the application. The app (the enforcement point) asks a
question, the engine (the decision point) answers it.
The question has a very familiar shape. Here is roughly what it looks like in AuthZEN, the OpenID Foundation's standard API between the two, finalized in early 2026:
1{2 "subject": { "type": "user", "id": "usr_ben" },3 "action": { "name": "approve" },4 "resource": { "type": "order", "id": "1042" },5 "context": { "time": "2026-09-19T10:24:00Z" }6}
Subject, action, resource. Plus context. The answer is { "decision": true }.
And the rule lives in one place, written in a language made for it. Acme's approval rule from the ABAC section, in Cedar (the policy language behind AWS Verified Permissions):
1permit (2 principal in Acme::Role::"approver",3 action == Acme::Action::"approve",4 resource is Acme::Order5)6when {7 resource.costCenter == principal.costCenter &&8 resource.amount <= principal.approvalLimit9}10unless {11 resource.requester == principal12};
Readable, testable, reviewable, versioned in git. It's the difference between "the rules are somewhere in the code" and "the rules are here".
Other engines worth knowing: OPA with its Rego language (the default choice in the Kubernetes and cloud-native world), OpenFGA and SpiceDB for ReBAC, Cerbos, Oso, Casbin. Different languages, same idea.
The rules are the easy part. The hard parts are:
- Data. The engine needs to know Ben's approval limit and the order's cost center. Either the app sends everything in the request, or the engine syncs data, or it fetches it. Each option has its own consistency and latency headaches.
- Lists. "Can Ben approve 1042?" is one check. "Show Ben every order he can approve" is a filtering problem, and naive loops over thousands of checks don't scale. Partial evaluation (OPA, Cedar) and ReBAC reverse lookups exist for exactly this.
- Enforcement. A perfect engine is useless if one endpoint forgets to ask it. Which is the forty-endpoints problem again.
One request, end to end
Let's put it all together. ExpenseBuddy reads Anna's monitor order.
- Identity. Anna's record,
usr_8f3a, lives in Acme's identity provider. - Authentication. She logs in with a passkey, at Acme, not at ExpenseBuddy.
- Delegation. She consents, and ExpenseBuddy gets an OAuth access token with
scope: orders:read,aud: purchasing, valid for 15 minutes. - Token validation. Acme Purchasing's API checks signature, issuer, audience, expiry. Also that the scope covers reading orders.
- Authorization. The API asks the policy engine: can
usr_8f3a, throughexpensebuddy,readorder:1041? Anna requested that order. Allow. - Response. ExpenseBuddy gets one order. Hers.
And at 2 AM, invoice-sync gets its own token through client credentials, using its workload identity instead of a stored secret, with a scope that lets it read approved orders and nothing else. (In the good version of the story, anyway. See "just give it admin".)
Every layer adds a piece of the answer:
| Layer | What it knows | |
|---|---|---|
| 1 | Identity provider | Who Anna is and how she proved it |
| 2 | OAuth token | That Anna delegated reading orders to ExpenseBuddy, until 10:39 |
| 3 | Scopes | Which kinds of actions the client may attempt |
| 4 | Policy engine | Whether this subject may do this action on this specific resource |
| 5 | API | Enforces all of it, or doesn't, and that's the bug |
Wrapping up: known actors, known requests
That's the machinery. It's genuinely good. Decades of hard-won lessons, a lot of it written after something went badly wrong in public. I don't want this note to read as a setup for "and it's all broken". It isn't.
But look at what every piece of it quietly assumes.
We know who the actor is. A human with a passkey, or a program with a workload identity, which somebody deployed on purpose.
And we know what it will ask for. Roles are designed by admins who know the jobs. Scopes
are designed by API developers who know what clients do. Policies are written for actions
somebody anticipated. When ExpenseBuddy asks for orders:read, it's because a developer wrote
code that reads orders, and that code will do exactly that, every time.
The human decides and clicks. Or the developer decided, months ago, and the code executes. Either way, by the time the request hits the API, the intent behind it was settled by someone we can point at.
Which is most software. Or was.
What's next
Next note: delegation. OAuth already showed the basic idea, Anna handing ExpenseBuddy a slice of her authority. But what does it actually mean for software to hold authority that started with a human? What's the difference between acting as someone and acting for them? And what happens when the thing holding the valet key decides for itself where to drive?
Catch you in the next one.