Skip to content

VOL 08 / CH 03 / LESSON 03

3.3 Authorization Models: RBAC, ABAC, ReBAC, and Resource-Level Checks ​

Authentication establishes the principal. Authorization decides which actions that principal may perform on a specific resource in the current context. Many access-control failures occur when applications only check "logged in" or "has a certain role," without verifying the relationship between the target resource and the subject.

A Decision Has Four Inputs ​

text
allow? = policy(subject, action, resource, context)
  • subject: Users, services, devices, and their organizations, roles, and authentication guarantees;
  • action: read, update, approve, transfer, rather than HTTP methods alone;
  • resource: specific orders, projects, fields, tenants, and current status;
  • context: time, network, device risks, change windows, and request origin.

Security defaults to deny. High-risk operations should never be silently permitted when there's no matching allow rule, missing policy data, policy engine timeout, or uncertain identity.

RBAC Manages Stable Job Responsibilities ​

text
user → role → permissions

RBAC is suitable for stable responsibilities like "auditors can read reports" and "release managers can approve releases." Roles should reflect business responsibilities, not create a role for each page. If combinations like admin-east-read-except-finance-temp explode, it indicates that additional attributes or relationships are needed.

Don't write admin as a boolean value that bypasses all authorization. High-privilege operations should also have clear actions, scopes, re-authentication, and auditing, and allow separation between regular accounts and temporary privilege sessions.

ABAC Expresses Constraints Through Attributes ​

ABAC can combine conditions:

text
subject.department == resource.department
AND resource.classification <= subject.clearance
AND context.time within approved_window

The classification and clearance levels need an explicit ordering; comparing raw label strings lexicographically would be wrong. Attributes need trustworthy sources, types, freshness, and ownership. If users can modify their own department claims, the policy cannot rely on them. The policy language should include testing, change reviews, and explainable decision logs to avoid scattered conditional branches across dozens of controllers.

ReBAC Describes Relationships to Objects ​

Collaborative documents, code repositories, and social products often rely on relationships:

text
user:lin is member of team:atlas
team:atlas is editor of project:north
rule: project editors may edit documents belonging to that project
therefore user:lin can edit document:x in project:north

ReBAC can express inheritance, sharing, and organizational hierarchies, but it requires control over traversal depth, cycles, cache consistency, and delayed relationship revocation. Simply replacing object IDs from 17 with UUIDs only reduces enumeration convenience and cannot substitute for relationship checks on every resource access.

IDOR/BOLA from Missing Resource-Level Authorization ​

Dangerous process:

text
GET /orders/8172
→ Verify that the user is logged in
→ SELECT * FROM orders WHERE id = 8172
→ Return

Queries should incorporate authorization scopes early in the data access process:

sql
SELECT id, tenant_id, owner_id, status
FROM orders
WHERE id = :order_id
  AND tenant_id = :principal_tenant
  AND owner_id = :principal_id;

More complex permissions can first be determined by the policy engine to define allowed scopes or relationships, then queried against objects. Regardless of the architecture used, list, detail, update, export, batch interfaces, and background tasks must all perform consistent checks. Hidden frontend buttons can reflect permissions in the interface, but cannot stop direct requests.

Test Identical Object IDs in Different Tenants ​

This SQLite in-memory example executes the parameterized query above. Its only read rule is “in the principal’s tenant and owned by that principal.” principal_tenant and principal_id must come from verified server-side identity context. Function parameters are not automatically trustworthy; do not pass self-declared request-body values directly.

python
import sqlite3

db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE orders (id INTEGER, tenant_id TEXT, owner_id INTEGER, status TEXT)")
db.executemany("INSERT INTO orders VALUES (?, ?, ?, ?)", [
    (8172, "north", 101, "open"),
    (8173, "north", 102, "open"),
    (8172, "south", 101, "open"),
])

def read_order(order_id, principal_tenant, principal_id):
    return db.execute("""
        SELECT id, tenant_id, owner_id, status FROM orders
        WHERE id = :order_id
          AND tenant_id = :principal_tenant
          AND owner_id = :principal_id
    """, {"order_id": order_id, "principal_tenant": principal_tenant,
          "principal_id": principal_id}).fetchone()

assert read_order(8172, "north", 101) == (8172, "north", 101, "open")
assert read_order(8173, "north", 101) is None
assert read_order(8172, "south", 102) is None
assert read_order(8172, "west", 101) is None
assert read_order(9999, "north", 101) is None
db.close()

Remove the tenant predicate and test identical object IDs across tenants. Remove the owner predicate and test another user’s object within the same tenant. Updates and approvals also need object state and authorization conditions in the same atomic write or transaction to avoid a permission changing between check and use.

Carry Tenant Boundaries Through Every Data Path ​

The tenant must not come solely from a client header. When users can switch tenants, the server must verify membership and permission for the selected tenant. The tenant must be determined from authenticated identity and server-side routing context, and this tenant identifier must be propagated across database queries, cache keys, object storage paths, queue messages, search indexes, and log access.

If caching relies solely on resource_id, it might return Tenant A's objects to Tenant B; if backend workers trust the tenant in the message without verifying the task initiator, they could execute across boundaries. Tenant isolation is a property of the data model and execution context, not a filter in the API gateway.

Confused Deputy: A Privileged Service Is Misused ​

A transformation service has the privilege to read all objects, and if a user submits any source_url for it to read internal management addresses, this service becomes a confused deputy. Mitigations include:

  • Pass or obtain restricted delegated credentials for downstream calls made on a user’s behalf;
  • Bind audience, resource, action, and tenant to the token;
  • Intersect the service’s permissions with the user’s permissions;
  • Reject arbitrary resource references; parse them on the server and apply an allowlist;
  • Retain the original principal chain and auditing for cross-boundary operations.

Centralized Policy Can Be Evaluated Locally ​

We can centralize policy definitions in a versioned policy repository and choose among sidecar, embedded evaluator, or local caching based on latency and availability. Define:

  • When the policy decision point is unavailable, which actions fail closed;
  • How long does policy and relationship data propagate, and what is the revocation window?
  • Whether cached decisions cover subject, resource, action, tenant, policy/relationship versions, and relevant context, with TTLs within the allowed revocation window;
  • Does the decision log record input summaries, rule versions, and results, while avoiding disclosure of sensitive information;
  • Does emergency deny have a distribution path faster than a regular release?

High-risk actions can use just-in-time elevation with temporary, resource-limited access, approval, and automatic expiration. Permanent superadmin privileges expand the attack surface and make routine operations harder to distinguish from emergencies during an audit.

Authorization Test Matrix ​

text
Positive: The correct subject can perform the allowed action
Horizontal: Same-role users cannot access another user’s resources without permission
Vertical: Regular users cannot perform high-privilege actions
Cross-tenant: No resource references are allowed to cross tenant boundaries
Status: Closed/Frozen: object refuses an action that was previously allowed
Revocation: Revoking a relationship or role denies access within the agreed window.
Fault: Fails as designed when the policy service/attribute source is unavailable
Batch: Lists, exports, and async tasks behave consistently with single-object checks

No matter how strong the authentication, it won't automatically grant correct authorization. The true backbone of a permission system lies in placing resource-level checks within the server's trusted boundary and making policies testable, explainable, and revocable.

References ​

Built with VitePress | Software Systems Atlas