This lesson on Authorization — Roles & Permissions is hands-on and example-driven. You will design and implement a defense-in-depth authorization system in FastAPI spanning RBAC, contextual ABAC policies, centralized policy enforcement, and native database query scoping. You will secure multi-tenant architectures against mass assignment, privilege escalation, and memory-filtering leaks.
What You'll Be Able To Do
- Differentiate authentication identity from authorization access checks across service boundaries.
- Implement a centralized
can(user, action, resource)policy enforcement function that fails closed. - Transition simple RBAC models to dynamic ABAC policies to prevent role explosion.
- Enforce field-level redaction and prevent mass assignment vulnerabilities using strict schemas.
- Inject tenant and ownership scopes directly into ORM queries instead of filtering in RAM.
Detailed Concept Walkthrough
1. Authentication vs. Centralized Authorization
Authentication confirms who a user is (identity verification), while authorization determines what actions they are permitted to perform on specific resources. Verifying identity must never imply blanket access across the system.
- Separation of Concerns: Authentication produces a verified identity token (such as a JWT or session identifier), but every sensitive route must execute an explicit authorization check after identity is established.
- Centralized Service Gatekeeper: Direct database access must be forbidden across UI, API, and background workers, channeling all read/write operations through a centralized service layer that acts as the single gatekeeper.
- UI vs. Backend Reality: Client-side UI controls like hidden buttons or disabled inputs exist solely for user experience and offer zero security; true enforcement must happen on the backend server.
from fastapi import FastAPI, Depends, HTTPException, status
app = FastAPI()
# AuthN: Identity resolution
def get_current_user(token: str) -> dict:
# In production, decode/verify token signature
return {"id": "usr_1", "role": "member", "tenant_id": "tenant_a"}
# AuthZ: Permission gatekeeping
@app.delete("/documents/{doc_id}")
def delete_document(doc_id: str, current_user: dict = Depends(get_current_user)):
if not can(current_user, "delete", doc_id):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied")
return {"status": "deleted"}
Key Takeaway: Authentication proves identity, but centralized backend authorization gates every single action.
2. RBAC to ABAC Evolution
Role-Based Access Control maps permissions directly to static roles, while Attribute-Based Access Control dynamically evaluates user, resource, action, and environment attributes.
- Role Explosion Problem: RBAC works well for coarse, job-title-based boundaries, but adding conditional rules (such as resource ownership or document status) causes an unmaintainable explosion of micro-roles.
- Dynamic Evaluation: ABAC replaces sprawling role tables with rule-based predicates that evaluate contextual metadata including user attributes, resource state, and environmental context.
- Auditability and Maintainability: ABAC simplifies administration by expressing complex business policies (e.g., 'only the author may edit drafts that are unlocked') in structured policy code.
from dataclasses import dataclass
@dataclass
class Document:
id: str
owner_id: str
is_locked: bool
status: str
def can_edit_document(user: dict, doc: Document) -> bool:
# ABAC: Evaluates user ID against document owner, status, and lock state
is_owner = user["id"] == doc.owner_id
is_draft = doc.status == "draft"
not_locked = not doc.is_locked
return is_owner and is_draft and not_locked
Key Takeaway: Switch from RBAC to ABAC when access logic demands dynamic resource ownership and state conditions.
3. The Universal Policy Enforcer (can)
A single centralized decision function provides the single source of truth for authorization decisions across the entire application stack.
- Deterministic Evaluation: The
can(user, action, resource)function accepts context and returns a strict Boolean indicating whether the action is permitted. - Fail-Closed Philosophy: If a resource is missing, permissions are undefined, or an unhandled edge case occurs, the policy engine defaults to denying access.
- Cross-Stack Consistency: The UI calls this policy engine to render appropriate controls, while the backend API consumes it to raise 403 Forbidden exceptions.
def can(user: dict, action: str, resource: object) -> bool:
"""Universal policy decision point: fails closed by default."""
# Global administrator bypass or specific domain rules
if user.get("role") == "admin":
return True
if action == "edit" and isinstance(resource, Document):
return user.get("id") == resource.owner_id and not resource.is_locked
# Default fail-closed fallback
return False
Key Takeaway: Route all access checks through a single, fail-closed policy function across the stack.
4. Field-Level Security and Scoping
Authorization must operate at the field level on writes and reads, while scoping all database queries natively to prevent data leakage.
- Mass Assignment Prevention: Attackers exploit unconstrained ORM updates to inject elevated fields like
role: admin; write operations must strictly whitelist mutable fields. - Field Redaction: Read payloads must filter or mask sensitive fields (such as internal notes or PII) based on the requester's permission level before serialization.
- Native Query Scoping: Fetching all database records into application RAM before filtering creates security risks and performance bottlenecks; tenant constraints must reside in the SQL
WHEREclause.
from pydantic import BaseModel
from sqlalchemy.orm import Session
# Mass assignment protection via explicit Pydantic schema
class DocumentUpdateSchema(BaseModel):
title: str
body: str # Explicitly excludes 'is_locked' and 'role'
def get_tenant_documents(db: Session, tenant_id: str):
# Native DB-level scoping: unauthorized tenant data never leaves database
return db.query(DocumentModel).filter(DocumentModel.tenant_id == tenant_id).all()
Key Takeaway: Whitelist fields explicitly on writes and enforce tenant boundaries natively in SQL queries.
Topics Covered in Authorization — Roles & Permissions
- AuthN vs AuthZ (0:41 - 1:09) — Identity verification is distinguished from permission access control.
- Centralizing Authorization Logic (1:10 - 2:14) — Backend service layers enforce security while UI visibility provides user convenience.
- RBAC and Role Explosion (2:15 - 3:13) — Static roles simplify initial designs but become unmaintainable under complex conditions.
- Attribute-Based Access Control (3:14 - 4:18) — Dynamic access policies evaluate user, resource, action, and environment attributes.
- The Universal
canEnforcer (4:18 - 5:13) — A single centralized decision function provides a consistent source of truth. - Field Precision and Mass Assignment (5:14 - 6:36) — Sensitive payload fields are redacted and write payloads are strictly whitelisted.
- Database Scoping (6:36 - 7:28) — Tenant and ownership boundaries are applied directly inside database queries.
- Defense in Depth Strategy (8:02 - 9:34) — Multiple independent architectural layers enforce security with fail-closed defaults.
FastAPI Cheat Sheet
-
can(user, action, resource)— Evaluates authorization policy returning a Booleanis_allowed = can(current_user, "publish", doc) -
Fail Closed Default— Rejects access if permissions are ambiguousreturn False # Fallback in policy enforcer -
Explicit Pydantic Whitelist— Prevents mass assignment on write operationsclass DocUpdate(BaseModel): title: str -
Native Query Scoping— Enforces tenant isolation in SQL layerquery.filter(Model.tenant_id == user.tenant_id) -
HTTP 403 Forbidden— Rejects authenticated requests lacking required permissionsraise HTTPException(status_code=403, detail="Forbidden")
Comparison Table
| Dimension | Role-Based Access Control (RBAC) | Attribute-Based Access Control (ABAC) |
|---|---|---|
| Decision Basis | Static user roles | Dynamic user and resource attributes |
| Scalability | Suffers from role explosion | Scales via flexible rule logic |
| Context Awareness | Ignores resource status or state | Evaluates ownership, locks, and environment |
| Complexity | Low to start with | Higher initial architecture setup |
Common Pitfalls
- Mistake: Relying on hidden UI elements for security. Avoid: Enforcing every authorization policy strictly in the backend service layer.
- Mistake: Passing raw client request dictionaries into ORM models. Avoid: Using explicit Pydantic schemas to whitelist writeable fields.
- Mistake: Loading entire tables into RAM and filtering in Python. Avoid: Injecting tenant and user constraints directly into database queries.
FAQs
- Why is fetching all data and filtering in Python considered a severe vulnerability? It leaks cross-tenant records into application memory, creates severe performance bottlenecks, and risks exposing unredacted data if any downstream transformation fails.
- What is role explosion and how does ABAC solve it? Role explosion occurs when every edge case demands a distinct static role; ABAC eliminates this by evaluating dynamic attributes like ownership and status.
- What does failing closed mean in policy enforcement? Failing closed means any missing context, runtime exception, or unhandled permission state automatically results in an explicit denial of access.