Home › Security › Authentication & authorization
guide low

Authorization models (RBAC, ABAC, ReBAC)

Problem An app with several kinds of users or shared data must decide who may do what to which object. This guide helps choose between roles, attributes and relationships, and where to put the checks.

How to decide who may do what to which object; roles for coarse rules, relationships for sharing, and checks on every object.

authorizationrbacabacrebaczanzibaraccess-controlmulti-tenancy
Overview

Authorization decides whether this principal may perform this action on this resource. Broken access control is the most common serious web vulnerability, usually because a handler forgot to check the specific object ("can read invoices" vs "can read invoice 42"). Put the decision in one function, call it everywhere, default to deny.

Key facts

Use when

  • Designing permissions for an app with more than one kind of user or any data sharing.
  • Banks and telecoms: policy-driven access to internal apps and platforms; media, cloud and IoT platforms: per-service and per-device permissions.

Avoid when

  • Single-user tools: an authenticated-or-not check is enough.

Deep dive

Decision table

SituationChoiceWhy
Internal tool, few fixed rolesRBAC in code (enum of roles → permissions)Easy to reason about and test
B2B SaaSTenant isolation (RLS) + per-tenant RBACTenant leaks are the worst failure
Docs/folders/projects shared with users and groupsReBAC: SpiceDB, OpenFGA, or a relation tableInheritance and sharing are graph queries
Rules on context (region, clearance, time, device posture)ABAC with a policy engine (Cedar, OPA/Rego)Policies outside code, testable
Many services need the same decisionsCentral policy service or embedded engine with shared policy bundleOne source of truth, consistent audit
Customer-defined rolesRBAC with roles as data (role → permission rows)Customers edit roles without deploys

How it works

Parts:

  • policy function — allowed(principal, action, resource, context) -> bool, the one place rules live
  • principal — user or service with tenant, roles, attributes
  • resource — object with owner, tenant, sharing metadata
  • rule store — role table, attribute policies or relationship tuples
  • audit log — denials and permission changes
  1. Load the resource (or its ownership/tenant metadata).
  2. Evaluate tenant match, then role/relationship/attributes, then explicit denies.
  3. Deny unless a rule allows; log the denial with principal, action, resource ID.
request -> handler -> load resource -> allowed(p, a, r, ctx) -> allow: act
                                              |                 deny: 404/403 + log
                                  rules: roles | attributes | tuples

Zanzibar-style ReBAC stores tuples object#relation@subject (doc:42#editor@group:eng#member) and rewrites (editor implies viewer; folder viewer implies doc viewer). Checks are graph walks with caching; a consistency token ("zookie") prevents the new-enemy problem (stale ACL applied to new content).

Code

Rust: a single policy function with typed actions (no stringly permissions scattered in handlers).

#[derive(Clone, Copy, PartialEq)]
pub enum Role { Viewer, Editor, Admin }

#[derive(Clone, Copy)]
pub enum Action { Read, Update, Delete, Share }

pub struct Principal { pub user_id: u64, pub tenant_id: u64, pub role: Role }
pub struct Doc { pub id: u64, pub tenant_id: u64, pub owner_id: u64, pub shared_with: Vec<u64> }

pub fn allowed(p: &Principal, a: Action, d: &Doc) -> bool {
    if p.tenant_id != d.tenant_id {
        return false; // tenant boundary first, regardless of role
    }
    let owner = d.owner_id == p.user_id;
    let shared = d.shared_with.contains(&p.user_id);
    match a {
        Action::Read => owner || shared || matches!(p.role, Role::Editor | Role::Admin),
        Action::Update => owner || (shared && p.role != Role::Viewer) || p.role == Role::Admin,
        Action::Delete | Action::Share => owner || p.role == Role::Admin,
    }
}

// In a handler: load, check, then act. Return 404 (not 403) to avoid confirming existence.
pub fn get_doc(p: &Principal, id: u64, load: impl Fn(u64) -> Option<Doc>) -> Result<Doc, u16> {
    let d = load(id).ok_or(404u16)?;
    if !allowed(p, Action::Read, &d) { return Err(404); }
    Ok(d)
}

Postgres row-level security as a second line of defense for tenancy:

ALTER TABLE docs ENABLE ROW LEVEL SECURITY;
ALTER TABLE docs FORCE ROW LEVEL SECURITY;          -- applies to the table owner too
CREATE POLICY tenant_isolation ON docs
  USING (tenant_id = current_setting('app.tenant_id')::bigint)
  WITH CHECK (tenant_id = current_setting('app.tenant_id')::bigint);
-- per request, inside the transaction:
SET LOCAL app.tenant_id = '17';

The app must connect as a role without BYPASSRLS and not a superuser.

Cedar policy (ABAC/RBAC mix):

permit(principal, action == Action::"update", resource)
when { resource.owner == principal || principal in resource.editors };
forbid(principal, action, resource) when { resource.tenant != principal.tenant };

Efficiency

  • Cost: a check is a few comparisons in code; ReBAC engines add lookups. The Zanzibar paper reports 95th-percentile latency under 10 ms at millions of checks per second. Bound: latency of the data a check needs, not policy logic.
  • Levers:
    • Load the resource once and pass it to the policy — one fetch instead of several — every handler.
    • Push the filter into the query (WHERE, RLS, lookup-resources) — one query instead of N checks — list endpoints.
    • Bulk checks (SpiceDB, OpenFGA) — one call per page of objects — ReBAC services.
    • Consistency-token caching (zookie) — the Zanzibar result above — large graphs.
    • Index tenant_id first in composite indexes — keeps RLS predicates indexable — multi-tenant tables.
  • Naive → standard → state of the art:
    • Naive · per-row check calls, fetch-then-filter in memory
    • Standard · in-process role enum plus tenant_id check, filters in SQL
    • State of the art · cached, batched ReBAC service (Zanzibar-style) or embedded Cedar, whose paper reports better evaluation performance than OpenFGA and Rego

Implementation notes

  • Enforce in two places: the policy check in the service layer, and tenant scoping in the data layer (RLS or a query builder that always adds tenant_id).
  • List endpoints: filter in the query (WHERE derived from the policy, or a ReBAC "lookup resources" call), never fetch everything and filter in memory.
  • Test the policy as a table: principals × actions × resources → expected result, including cross-tenant cases.
  • Log every denial and every permission change; access reviews need them.
  • Version policies like code: reviewed, tested, deployed through CI.

Pitfalls

  • IDOR/BOLA: GET /invoices/:id checks login but not that the invoice belongs to the caller. The top API vulnerability (OWASP API1).
  • Checks in the UI only (hidden buttons) and not on the server.
  • Filtering list endpoints differently from detail endpoints; search, export and GraphQL resolvers often skip checks.
  • Trusting client-sent tenant IDs or roles (from body, or unverified JWT claims).
  • Role checks like role == "admin" spread over 200 handlers: impossible to audit. Centralize.
  • Caching decisions without invalidation on role change.
  • Mass assignment: user updates {"role":"admin"} through a generic update endpoint.
  • RLS with SET instead of SET LOCAL on pooled connections: tenant ID leaks to the next request.

In production

  • Media: Netflix enforces access control in microservices with OPA (OPA adopters); Google runs Zanzibar-style ReBAC for Drive, Photos, YouTube and Maps (Zanzibar paper).
  • Finance: BNY Mellon uses OPA as a sidecar for application access control based on Active Directory context (OPA adopters).
  • Telecom: VodafoneZiggo uses OPA for authorisation decisions in its internal developer platform (OPA adopters).
  • Cloud infra and IoT: Canonical uses OpenFGA as the permission backend for Juju, LXD and Ubuntu Pro; Mapped uses it for IoT identity authorizations (OpenFGA adopters).

Implementations

namelangrepoinstallverdictlicencehealthas_ofnotes
cedar-policyRustcedar-policy/cedarcargo add cedar-policy————embedded RBAC/ABAC-style policy language with a validator; CLI: cargo install cedar-policy-cli
casbinRustapache/casbin-rscargo add casbin————ACL, RBAC, ABAC models from config files
OPAGoopen-policy-agent/opabrew install opa————Rego policies; sidecar, library or server
SpiceDBGoauthzed/spicedbbrew install spicedb————Zanzibar-style ReBAC service
OpenFGAGoopenfga/openfgabrew install openfga————Zanzibar-style ReBAC service (CNCF)

Sources

  • Pang et al., Zanzibar: Google's Consistent, Global Authorization System, USENIX ATC 2019. paper
  • Sandhu, Coyne, Feinstein, Youman, Role-Based Access Control Models, IEEE Computer, 1996. paper
  • Hu et al., NIST SP 800-162: Guide to Attribute Based Access Control (ABAC) Definition and Considerations, 2014. spec
  • OWASP, API Security Top 10, 2023. doc; Authorization Cheat Sheet. doc
  • PostgreSQL documentation, Row Security Policies. docs
  • Cutler et al., Cedar: A New Language for Expressive, Fast, Safe, and Analyzable Authorization, 2024. paper · code
  • OPA project, Adopters. list; OpenFGA, Adopters. list

Used by

Systems (86):

Alternatives and related