---
key: "authorization-models"
title: "Authorization models: RBAC, ABAC, ReBAC and policy as code you can test"
url: "https://softwaresecurity.com/domains/identity-crypto/identity/authorization-models/"
api: "https://softwaresecurity.com/api/v1/articles/authorization-models.json"
domain: "identity-crypto"
topic: "Identity \u0026 cryptography"
type: "Guide"
identifiers: ["CWE-863","CWE-862","CWE-639","A01:2025","API1:2023","API3:2023","API5:2023","V8.2.1","V8.2.2","V8.2.3","V8.3.1","V8.3.3","V8.4.1","CWE-284"]
tags: ["Access control","Infrastructure as code","API security"]
stages: ["design","build"]
roles: ["developer"]
license: "CC-BY-4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
code_license: "MIT"
---

# Authorization models: RBAC, ABAC, ReBAC and policy as code you can test

Translate roles, attributes and relationships into explicit subject-action-resource decisions, prevent broken object level authorization (BOLA), and verify ownership and tenant boundaries with tests.

Canonical URL: https://softwaresecurity.com/domains/identity-crypto/identity/authorization-models/

## In short

Decide every request as subject, action and resource: take the subject from the verified credential, load the resource from your database, and allow only what an explicit rule permits. Check ownership and tenant on every object (this prevents BOLA, also called IDOR), allowlist writable fields, and check each function’s role. Roles, attributes and relationships can all express the rules; whichever you choose, keep the decision in one tested policy that every entry point calls.

- Authentication supplies the subject; authorization decides this action on this resource now.
- Load resource facts from the database inside the caller’s tenant scope, never from the request body.
- BOLA (API1), field-level BOPLA (API3) and function-level BFLA (API5) are separate checks; test each.
- RBAC, ABAC and ReBAC are ways to express rules; mature systems usually combine them.
- Deny by default when a fact is missing, and enforce state conditions in the same transaction as the change.
- Policy engines help only when every endpoint, job and export calls them with the right facts.

## Verify it

1. Request another user’s invoice in your own tenant, and your own invoice ID in another tenant: both must be denied without revealing existence.
2. Submit extra fields such as `role` or `tenant` in an update and confirm they are ignored or rejected.
3. Call every function an ordinary user should not reach, such as approval and export, and confirm `403` or `404`.
4. List and search endpoints return only rows the caller could read one at a time.
5. Run the policy tests in CI so that a rule change without a matching test fails the build.

## What you will learn

- Express authorization using trusted subject and resource facts.
- Separate object access from field-update permissions.
- Verify actual authenticated requests against negative ownership cases.

Audience: Developers who implement access checks in APIs and services, and reviewers who check object, field and function authorization.

Before you start:

- Express actor/action/object/context decisions and distinguish role from object relationship.

Time: About 20 minutes: 6 to read and 14 for the exercise

## Authentication supplies a subject; authorization evaluates an action

An authorization decision answers whether this subject may perform this action on this resource in the current context. A valid login or token supplies identity evidence, but the resource’s owner, tenant, state, and relationships determine which operations are allowed. Every value has a source: signed claims, trusted database fields, policy configuration, or untrusted request input.

Role-based access control groups permissions into roles such as billing-reader. Attribute-based rules combine facts such as tenant, document status, or required assurance. Relationship-based rules use links such as owner, team member, or editor. These models can coexist; naming a model does not establish that its implementation evaluates the right resource.

For a private invoice rule, allow read only when `claims.tenant` equals `invoice.tenant` and `claims.sub` equals `invoice.owner`. The invoice fields come from the trusted database lookup, not the request body. A role alone may be too broad, and a tenant match alone lets peers read one another’s private invoices. Missing this check is broken object level authorization (BOLA), the first risk in the OWASP API Security Top 10 2023 and better known in web applications as insecure direct object reference (IDOR); the fix card at /fix/missing-authorization/ shows the check in each stack.

## Which authorization model fits: RBAC, ABAC or ReBAC?

The models differ in which facts a rule can use, not in how secure they are. Most products start with roles, add attributes such as tenant and status, and use relationships where sharing between users matters. Choose a policy engine only after the decision table exists, because an engine evaluates the facts you give it and cannot find the ones you forgot.

| Model | A rule looks like | Good fit | Engines and services |
| --- | --- | --- | --- |
| RBAC (roles) | `billing-approver` may approve invoices | Stable job functions and admin areas | Framework role checks; Cedar; OPA |
| ABAC (attributes) | Same tenant, status is draft, request within business hours | Tenancy, data classification, state rules; NIST SP 800-162 | OPA with Rego; Cedar (Amazon Verified Permissions) |
| ReBAC (relationships) | Viewer of a document if member of a team that owns its folder | Sharing, nested groups and hierarchies | OpenFGA and SpiceDB, both modeled on Google Zanzibar |

## Write the decision before choosing a policy engine

Create a table of actions and resource states. Reading an invoice, changing its payee, approving it, and exporting a tenant’s entire ledger are different permissions. Include exceptional roles and who may grant them. A support agent may need a time-bounded audited grant rather than a permanent global bypass.

Deny by default when required facts are missing or malformed. Decide how to handle deleted users, suspended memberships, stale role claims, and relationship cycles. A cache speeds decisions but can retain access after revocation, so document its invalidation and maximum staleness.

Separate authorization of the target resource from authorization of fields being changed. A caller who may update their display name should not be able to submit `role=admin` or `tenant=other` through a generic object merge. An allowlist of writable fields preserves this boundary more clearly than deleting a few known-dangerous fields.

Bulk and list endpoints must apply the same scope as single-object reads. Fetching every row and filtering only in the UI leaks data. Pagination, totals, search suggestions, exports, and error messages can expose unauthorized records even when the final detail page returns denial.

## Keep checks next to the trusted resource and mutation

Load or query the resource within the caller’s authorized tenant scope, then evaluate the action against trusted attributes. When possible, express ownership predicates in the database query as well as the policy layer so unauthorized rows are never returned to the caller. Use consistent denial semantics where object existence itself is sensitive.

Mutation authorization can race with state changes. If approval requires `status=draft`, enforce that condition in the transaction or conditional update that commits approval. Checking draft in one query and writing later can authorize an operation after the invoice has changed state.

Central policy functions improve consistency only if every entry point calls them with the correct facts. Background jobs, GraphQL resolvers, exports, and internal APIs need the same trust analysis. A service-to-service call does not automatically inherit the end user’s authority unless the delegation and scope are explicit.

The “Secure a real HTTP API” project in the local project section below puts this rule into a running service. Use a supported Node.js release (22 or later) and npm, run `npm ci --ignore-scripts` and then `npm test`. The project starts a loopback Express API and performs actual JOSE token verification. In `exercise.mjs`, repair `canReadInvoice` to require both tenant and owner identity, and `selectProfileUpdate` to validate and select the allowed `displayName` field. The update contract requires an object with a string `displayName` whose trimmed value is nonempty and whose submitted length is at most 80 characters; return the trimmed value in a new object containing no other submitted fields. The IDOR lab at /labs/authorization/ practices the same object check in the browser.

## Express the invoice rules as tested policy in Rego

Policy as code puts the decision table in one versioned file with its own tests. This Rego policy for Open Policy Agent allows an owner to read an invoice in their tenant, and an approver to approve a draft in their tenant that is not their own; everything else is denied. The service passes the verified claims as `subject` and the invoice row it loaded as `resource`. It was tested with OPA 1.20.2 (`opa test`): nine cases, including a same-tenant peer, the same owner ID in another tenant, missing resource facts, self-approval, approval after the draft stage, and an unknown action.

```rego
package invoices

# Deny unless a rule below proves the request is allowed.
default allow := false

# Read: the verified subject and tenant must match the invoice loaded from the database,
# never values taken from the request body (BOLA, OWASP API1:2023).
allow if {
	input.action == "read"
	input.subject.tenant == input.resource.tenant
	input.subject.sub == input.resource.owner
}

# Approve: an approver in the same tenant, only while the invoice is a draft, never their own.
allow if {
	input.action == "approve"
	"billing-approver" in input.subject.roles
	input.subject.tenant == input.resource.tenant
	input.resource.status == "draft"
	input.subject.sub != input.resource.owner
}

```

## Use a matrix that keeps authentication and authorization distinct

The project’s ordinary owner request returns `200`. A peer in the same tenant and a matching owner ID in another tenant both receive `404`, matching its non-disclosure policy. Missing invoices also return `404`. Invalid signatures, expired tokens, wrong audience or token purpose, and missing credentials return `401` before resource authorization.

Profile updates accept a valid `displayName` while preserving the existing role and tenant even when those extra properties are submitted. Invalid field types return `400`; oversized JSON returns `413`; malformed JSON receives a bounded `400` response. These outcomes prove multiple boundaries, so preserve the tests rather than weakening expectations.

Production review must also test revocation, cached facts, bulk operations, and concurrent state changes. The project’s in-memory fixtures and single-process policy do not establish those deployment properties, but they give a concrete foundation for the subject-action-resource rule.

## Guided practice

Apply the invoice rule to supplied records, then repair the downloadable project.

### Supplied material

```text
Claims: sub=alice, tenant=acme (already cryptographically verified).
Invoice one: owner=alice, tenant=acme, total=80.
Invoice two: owner=bob, tenant=acme.
Invoice three: owner=alice, tenant=other.
Profile body: {displayName:"Al",role:"admin",tenant:"other"}.
Project: the Secure a real HTTP API download on this page; npm ci --ignore-scripts; npm test.
```

### 1. Build the decision table

Evaluate all three invoices using both required equalities.

Worked answer: One permits read. Two fails owner equality; three fails tenant equality. A same-tenant-only rule incorrectly permits two.

### 2. Constrain the mutation

Select and validate only the documented `displayName` field; leave stored role and tenant unchanged.

Worked answer: The response shows `displayName` Al, role member, tenant acme. A numeric `displayName` is rejected rather than coerced.

### 3. Run the actual boundary checks

Repair `exercise.mjs` and run the provided test suite without changing expectations.

Worked answer: The ordinary HTTP request succeeds; ownership/tenant probes deny; real bad-signature/audience/purpose cases remain 401; malformed and oversized input retains its bounded errors.

Deliverable: Repaired policy functions, passing test output, and a subject-action-resource matrix.

Scope: The project uses real HTTP and JOSE verification with temporary keys. It does not implement production persistence, distributed policy caches, or organization-wide role administration.

## Executable practice

- [Secure a real HTTP API: project ZIP](https://softwaresecurity.com/downloads/projects/express-api-519f6cf7960d8d96.zip) — Node.js 22+ · npm

Articles and teaching material: CC BY 4.0. Original executable code: MIT. Attribution: Software Security.

## Sources

- [OWASP Authorization Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html)
- [OWASP API Security Top 10 2023: API1 Broken Object Level Authorization](https://api-security.owasp.org/editions/2023/en/0xa1-broken-object-level-authorization/)
- [NIST SP 800-162: Guide to Attribute Based Access Control](https://csrc.nist.gov/pubs/sp/800/162/upd2/final). January 2014, updated August 2019.
- [Zanzibar: Google’s Consistent, Global Authorization System (USENIX ATC 2019)](https://research.google/pubs/zanzibar-googles-consistent-global-authorization-system/)
- [Open Policy Agent: policy testing](https://www.openpolicyagent.org/docs/policy-testing)
- [OWASP ASVS 5.0: V8 Authorization](https://github.com/OWASP/ASVS/blob/v5.0.0_release/5.0/en/0x17-V8-Authorization.md). Version 5.0.0 (May 2025).