Once you've authenticated the user and got a token — what may they actually do? The decision-making layer the rest of the Identity & Access series feeds into.
Every authorisation system separates "asking the question" from "answering it" from "where the rules live" from "where the data lives". The four-letter abbreviations come from XACML, but the architecture survives every modern engine.
{
"subject": { "id":"alice", "role":"editor", "team":"emea" },
"action": "document.publish",
"resource": { "type":"document", "id":"d_42",
"owner":"alice", "classification":"public" },
"context": { "ip":"203.0.113.5", "time":"2026-05-06T09:00Z",
"auth_strength":"mfa" }
}
{
"decision": "allow",
"obligations": [ "log:audit", "redact:ssn" ],
"explanation": "rule allow-editor-on-own-public-doc"
}
Obligations are side effects the PEP must apply (e.g. mask a field, log to audit). Explainability is what auditors actually want.
| Model | Era | The idea | Where it survives |
|---|---|---|---|
| DAC Discretionary Access Control | 1970s | The owner of a resource grants access to whoever they like. | Unix file permissions; shared-folder ACLs; Google Drive sharing. |
| MAC Mandatory Access Control | 1970s | Central authority assigns labels to subjects and resources; access is determined by lattice rules (Bell-LaPadula). | Government & defence systems; SELinux; AppArmor. |
| RBAC Role-Based Access Control | 1992 (Ferraiolo & Kuhn) → ANSI standard 2004 | Subjects are assigned roles; permissions are attached to roles. | Almost every enterprise app from 1995–2015. |
| ABAC Attribute-Based Access Control | 2000s — XACML 1.0 in 2003 | Decisions evaluated as predicates over attributes of subject, resource, action, environment. | Government, finance, healthcare; modern Policy-as-Code (OPA, Cedar) is ABAC at heart. |
| ReBAC Relationship-Based Access Control | 2019 (Google Zanzibar paper) | Authorisation derived from a graph of relationships between subjects and resources. | Modern collaboration apps (Google Drive, GitHub, Notion); OpenFGA, SpiceDB, Warrant. |
Each model is a different way to compress the boolean function "may subject S do action A on resource R?". RBAC compresses by clustering subjects (roles); ABAC by parameterising the condition (predicates); ReBAC by exploiting the structure of the resource graph (relationships).
Real systems mix models. Google Drive: ReBAC on documents, RBAC on workspace administration, ABAC for "external sharing only allowed for users with the EXTERNAL_OK attribute". The model is a vocabulary; production policy is the union.
admin ⊃ editor ⊃ viewer).approver or requester, never both.sudoers, Kubernetes Role/ClusterRole, AWS IAM groups.admin, editor, viewer. A customer asks "viewer that can also export". You add viewer-with-export.admin-no-billing.kind: Role
metadata: { name: pod-reader, namespace: dev }
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","list","watch"]
---
kind: RoleBinding
metadata: { name: alice-pod-reader, namespace: dev }
subjects: [{ kind: User, name: alice }]
roleRef: { kind: Role, name: pod-reader }
Instead of pre-clustering users into roles, ABAC writes a predicate over the request and the world. A role becomes a special case ("subject.role == admin"), but you can also express things RBAC can't: time-of-day, IP range, classification level, ownership.
permit (
principal,
action == Action::"document.publish",
resource is Document
) when {
resource.owner == principal
&& resource.classification == "public"
&& context.auth_strength == "mfa"
};
No roles in sight; the rule reads like the natural-language policy.
Google's 2019 paper "Zanzibar: Google's Consistent, Global Authorization System" described the system that powers Drive, Calendar, YouTube, Cloud, and most of Google's product surface. It's a relationship-based model implemented as a globally-consistent tuple store.
# object # relation @ user
doc:report1#owner@user:alice
doc:report1#viewer@user:bob
doc:report1#viewer@group:engineering#member
group:engineering#member@user:carol
folder:q3#parent@doc:report1
folder:q3#viewer@user:dan
Every authorisation fact is a small tuple. The graph is the policy.
type doc {
relation owner: user
relation viewer: user | group#member | doc#owner
permission view = viewer + owner
+ parent.viewer // inherit from folder
}
type folder {
relation viewer: user | group#member
}
type group {
relation member: user
}
Policy-as-Code (PaC) means: the rules live in version-controlled files, are reviewed in PRs, are tested with unit tests, and are deployed by a pipeline. The actual policy language varies — Rego, Cedar, internal DSLs — but the engineering practice is the same.
| Tool | Language | Sweet spot | Hosted by |
|---|---|---|---|
| OPA (Open Policy Agent) | Rego (datalog-ish) | General-purpose; outside-of-app PDP for k8s, Terraform, microservices | CNCF (graduated); Styra |
| Cedar | Cedar (purpose-built) | App-level RBAC + ABAC; safer / more analysable than Rego | AWS (open source); AVP service |
| OpenFGA | FGA-DSL + tuples (Zanzibar-like) | ReBAC at app scale; collaboration apps | Auth0/Okta, CNCF sandbox |
| SpiceDB | Schema + relationships (Zanzibar-like) | Same niche as OpenFGA; commercial AuthZed managed cloud | AuthZed |
| Casbin | Multi-model (RBAC, ABAC, ReBAC) | Embedded in many languages; small apps that prefer a library | open source |
| Permify / Topaz / Warrant | Variants on the above | Each scratches a slightly different operational itch | open / commercial |
| XACML / WSO2 IS | XACML 3.0 | Legacy enterprise; government / regulated environments | OASIS standard, WSO2 |
Two questions to ask. (1) What's your dominant model — RBAC, ABAC, ReBAC? (2) Where will the engine run — embedded, sidecar, central service? Most rejection of "the wrong tool" comes from forcing a Zanzibar-style graph onto a Rego rule, or vice versa.
package authz
default allow = false
allow if {
input.action == "document.publish"
input.subject.role == "editor"
input.resource.owner == input.subject.id
input.resource.classification == "public"
input.context.auth_strength == "mfa"
}
allow if {
input.subject.role == "admin"
not deny
}
deny if {
input.context.ip in data.blocked_cidrs
}
AWS published Cedar in 2023 (Apache 2.0 + the AWS-hosted Verified Permissions service). Designed from a formal-methods background: deliberately small, deliberately analysable, deliberately app-level.
permit (
principal in Group::"engineering",
action in [Action::"doc.read", Action::"doc.comment"],
resource is Document
) when {
resource.classification != "secret"
|| principal has clearance && principal.clearance >= 3
};
forbid (
principal,
action == Action::"doc.delete",
resource is Document
) unless {
principal == resource.owner
};
RBAC (principal in Group::"engineering") and ABAC (resource.classification) and ReBAC-lite (principal == resource.owner) all live together.
Infrastructure / k8s / IaC validation. That's still Rego/OPA territory.
// schema
model
schema 1.1
type user
type group
relations
define member: [user]
type document
relations
define owner: [user]
define editor: [user, group#member]
define viewer: [user, group#member]
define can_edit: editor or owner
define can_view: can_edit or viewer
// runtime tuples
write document:report1 owner user:alice
write document:report1 viewer group:engineering#member
write group:engineering member user:bob
// query
check document:report1 can_view user:bob → allow
check at the 99th percentile (with caches).list_objects — all docs Alice can view (for UI listings).list_users — everyone who can view this doc.Both are real databases. Treat them like one — backups, monitoring, schema migrations, capacity planning.
"Just import the SDK and call can()."
Best for: monolithic apps, latency-sensitive paths.
"localhost:8181 says yes."
Best for: microservices, k8s, polyglot stacks.
"call the AuthZ team's API."
Best for: organisations with strong central security teams; small apps that want SaaS authZ.
RBAC checks (course-grained) inside the app for low-latency hot path. ReBAC checks against a sidecar/external PDP for object-level decisions. ABAC overlay for "additional constraints" (geo, time, classification). The decisions agree because the schemas are unified, but each runs at the right cost.
Most production AuthZ latency is not the policy engine — it's the data lookups.
list_users & list_objects sub-ms.If your UI shows 50 documents and asks "which can I edit?", don't make 50 round-trips. Every modern engine has a batchCheck / checkBulk primitive — use it.
Calling the PDP from inside a database query / N+1. Authz must run before / after the query, not per row.
Every auth decision should be loggable as a structured record. Sample fields:
{ "ts": "2026-05-06T09:00:01.234Z",
"trace_id": "abc…",
"subject": { "sub":"alice", "iss":"https://idp/" },
"action": "document.publish",
"resource": { "type":"document", "id":"d_42" },
"decision": "allow",
"matched_rule": "allow-editor-on-own-public-doc",
"policy_version": "git:5f3a91c",
"engine": "cedar-3.2.1",
"latency_ms": 1.4 }
If your decision log + policy versioning can answer these, you're SOC 2 / HIPAA / FedRAMP-credible.
opa eval with --explain=full dumps the trace; usable in tests.check can return the path through the relationship graph.opa test. Cedar: built-in test harness. OpenFGA: store-test.An authorisation engine is only as good as the facts it gets about the subject. Those facts come from the rest of the Identity & Access stack.
| From the IdP / token | Becomes a PDP input |
|---|---|
OIDC sub | subject.id |
OIDC aud | resource scope check (ensure this token is for me) |
OIDC scope | action allow-list pre-filter |
OIDC acr / SAML AuthnContextClassRef | context.auth_strength ("mfa" / "password") |
OIDC amr | context.auth_methods (["pwd","webauthn"]) |
OIDC auth_time | context.auth_age_seconds |
SAML AttributeStatement.groups / SCIM-synced groups | subject.roles / membership tuples |
| Custom claims (department, region, clearance) | subject.<attribute> |
Re-fetching the user's roles from the database on every PDP call when they're already on the access token. Trust the IdP that issued the token; if you don't, that is your problem to fix.
If a policy demands context.auth_strength == "mfa" and the token only attests pwd, the PEP should issue a step-up — not flatly deny. With OIDC: redirect to /authorize?prompt=login&acr_values=mfa and try again.
| If your authorisation question is... | Best-fit model | Likely engine |
|---|---|---|
| "Does this user have a role with this permission?" | RBAC | In-app, or any engine in RBAC mode (Cedar, OPA) |
| "Does this user have this attribute combo at this time?" | ABAC | Cedar (analysable) or OPA/Rego (general) |
| "Has someone shared this object with this user, possibly transitively?" | ReBAC | OpenFGA / SpiceDB / Permify / AuthZed |
| "All of the above" | Hybrid | Engine that supports both — Cedar + Zanzibar-class side car, or AWS Verified Permissions + Cognito |
| "Is this Kubernetes manifest allowed?" | Policy-as-Code on infra | OPA / Gatekeeper |
| "Is this Terraform plan allowed?" | Policy-as-Code on IaC | OPA / conftest, HashiCorp Sentinel |
| "Can this microservice talk to that one?" | Network + workload identity (mTLS / SPIFFE) | Service mesh (Istio, Linkerd, Cilium) |
| "Can this database column be read by this user?" | Row/column-level security in DB + central PDP | Postgres RLS, Snowflake row access policies, plus PDP for the rule body |
Pick the questions first. Write down the ten authorisation questions the product needs to answer. The model that makes those questions short and natural is the right one. The model that requires more JOINs / more roles / more rules than questions is wrong.
if (user.role == ...) in the codebase. Tag each with the question it answers.user X has admin role on workspace W = workspace:W#admin@user:X.permit/forbid semantics.| Domain | Stack |
|---|---|
| Collaboration SaaS (docs, kanban, chat) | OIDC at the edge · OpenFGA / SpiceDB for object-level ReBAC · embedded RBAC for admin actions · ABAC overlay for data-residency rules. |
| B2B SaaS, multi-tenant | OIDC + SCIM-synced groups · Cedar or OPA per request · per-tenant policy bundles via Styra/AVP · row-level security in DB for defence-in-depth. |
| Kubernetes platform | Workload OIDC for service identity · OPA/Gatekeeper for admission · Kubernetes RBAC for role-binding · SPIFFE/SPIRE for service-to-service mTLS. |
| FinTech / Open Banking | FAPI 2.0 OIDC at the edge · Cedar-style ABAC for transaction policies · separate engine for fraud rules · audit log streamed to regulator. |
| Healthcare / FHIR | SMART-on-FHIR scopes (OAuth) · ABAC on patient-clinician relationships · ReBAC for care-team graphs · break-glass emergency access pattern. |
| Internal corporate IT | Workforce IdP (Entra/Okta) issues OIDC + SAML · SCIM provisions to apps · per-app RBAC · CAEP for session revocation. |
| Public cloud account | AWS IAM (resource-scoped) · AWS Verified Permissions / Cedar for app-level · OPA/Conftest for IaC. |
| Government / regulated | SAML / OIDC SSO · ABAC with classification labels · audit + dual control / SoD enforced at PDP · MAC-style label propagation in DB. |
Easy to ship, expensive to fix. Always default-deny; require an explicit permit.
SCIM provisioned the user out at 17:00. PDP cached "alice ∈ admins" for 60 min. Use change-events to invalidate, not just TTL.
scope aloneScopes are coarse — "can call this API", not "can call this method on this object". The PDP must check the resource too.
If you look at a JWT and decide both "is the token valid?" and "is this user allowed to call this method?" in one function, you've coupled two concerns that change at very different rates.
An admin role that bypasses every rule is the most common reason customers find their data was accessed by a support engineer they didn't expect. Subject every privileged action to the same logging + step-up MFA as regular users.
That feature now bypasses every authz check. Add the new feature to the existing engine — even if it's awkward — until the engine genuinely doesn't fit and you re-architect.
UI hides the "Delete" button if the user can't delete. Backend doesn't check. User opens devtools and POSTs DELETE /api/x. Authorise at the API, not just in the UI.
"Alice tried to view doc X" is not enough. Log the policy version, matched rule, decision and obligations. Half of post-incident analysis is this log.
OAuth Primer / Part 1 / Part 2 · Introduction / Advanced OpenID Connect · SAML 2.0 & SCIM · Authentication Methods.
Ferraiolo & Kuhn, Role-Based Access Control (1992) · ANSI INCITS 359-2004 RBAC · OASIS XACML 3.0 · Pang et al., Zanzibar: Google's Consistent, Global Authorization System (USENIX 2019) · OPA — openpolicyagent.org · Cedar — cedarpolicy.com · OpenFGA — openfga.dev · SpiceDB / AuthZed — authzed.com · AWS Verified Permissions · NIST SP 800-162 (ABAC) · SP 800-205 (ABAC for Cloud)
Authentication tells you who. Tokens tell apps that. Authorisation is the actual rules — and rules belong in version-controlled code, not scattered through the application.