Home/Learn/FastAPI/RBAC & Scopes — Roles, Permissions, Ownership

RBAC & Scopes — Roles, Permissions, Ownership

Advanced
Auth & Security

Layer authorization: coarse roles via require_role, fine-grained permissions via a role→permission map, OAuth2 scopes via SecurityScopes when tokens must carry grants — plus the ownership checks RBAC alone misses.

Overview

Authentication says who you are; authorization says what you may do — and it layers. Coarse role gates (student/tpo/admin) reuse the require_role dependency factory, now reading the role from JWT claims. As products grow, checking permissions beats checking roles: a role→permissions mapping means "can this user drives:create?" survives every reorganisation of what a TPO is allowed to do — add a role, touch one dict, no endpoint changes. OAuth2 scopes bake grants into the token itself, and FastAPI's SecurityScopes + Security() machinery enforces per-endpoint scope requirements with docs support — the right tool for third-party API access and service-to-service calls. The layer everyone forgets until the pentest: object ownership. Role checks pass, but student 7 must still not read student 9's offer letter — resource-level checks (IDOR protection) belong in the service layer, on every single object access.

Roles → Permissions — Check Capabilities, Not Titles

The JWT carries the role; a single mapping expands it to permissions; require_permission asks "can they?", not "are they?". New roles or reshuffled powers touch the map, never the endpoints.

One policy map; endpoints declare capabilities they need
from fastapi import Depends, FastAPI, HTTPException

app = FastAPI()

ROLE_PERMISSIONS = {
    "student": {"drives:read", "applications:create", "profile:write"},
    "tpo":     {"drives:read", "drives:create", "drives:close",
                "students:read", "reports:read"},
    "admin":   {"*"},                                # everything
}

def require_permission(permission: str):
    def checker(user: dict = Depends(get_current_user)) -> dict:   # JWT chapter
        perms = ROLE_PERMISSIONS.get(user["role"], set())
        if "*" not in perms and permission not in perms:
            raise HTTPException(403, f"requires permission: {permission}")
        return user
    return checker

@app.post("/drives", status_code=201)
def create_drive(user: dict = Depends(require_permission("drives:create"))):
    return {"created_by": user["email"]}

@app.get("/reports/placements")
def placement_report(user: dict = Depends(require_permission("reports:read"))):
    return {"placed": 212, "total": 260}

# Why permissions beat raw roles:
#   "TPOs can now also close drives" → add one string to the tpo set. Done.
#   New "placement-coordinator" role → one new dict entry. Done.
#   With require_role("tpo") sprinkled on endpoints, both changes mean
#   hunting every endpoint that mentions a role. The map is the policy.

# At scale the map moves to the DB (editable per tenant), and the
# gyaan-api pattern applies: org_id scoping + role per membership.

OAuth2 Scopes with SecurityScopes + the Ownership Layer

Scopes put grants inside the token — enforced by Security(), rendered in /docs. And below every role system sits the check that stops IDOR: does THIS user own THIS object?

Scopes travel in the token; ownership checks stop IDOR
from fastapi import Depends, FastAPI, HTTPException, Security
from fastapi.security import OAuth2PasswordBearer, SecurityScopes
import jwt

app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(
    tokenUrl="token",
    scopes={"drives:read": "View drives", "drives:write": "Manage drives",
            "reports:read": "View reports"},        # documented in /docs Authorize
)

def current_user_scoped(security: SecurityScopes,
                        token: str = Depends(oauth2_scheme)) -> dict:
    claims = jwt.decode(token, SECRET, algorithms=["HS256"])
    granted = set(claims.get("scopes", []))          # scopes live IN the token
    missing = [s for s in security.scopes if s not in granted]
    if missing:
        raise HTTPException(
            403, f"missing scopes: {missing}",
            headers={"WWW-Authenticate":
                     f'Bearer scope="{security.scope_str}"'},
        )
    return {"email": claims["sub"], "scopes": granted}

@app.get("/reports/summary")
def summary(user: dict = Security(current_user_scoped, scopes=["reports:read"])):
    return {"ok": True}
# Scopes vs roles: scopes = what THIS TOKEN may do (third-party grants,
# service-to-service, least-privilege CI tokens); roles = what the USER is.
# Many systems need both: role for humans, scoped tokens for machines.

# ── The layer RBAC misses: OWNERSHIP (IDOR) ──
@app.get("/applications/{app_id}")
def get_application(app_id: int, user: dict = Depends(get_current_user)):
    application = load_application(app_id)           # service/repo layer
    if not application:
        raise HTTPException(404, "not found")
    if user["role"] != "tpo" and application.student_email != user["email"]:
        raise HTTPException(404, "not found")        # 404, not 403: don't confirm
    return application                               # the object even EXISTS
# Student 7 fetching /applications/9's data despite valid auth = IDOR,
# the most common real-world API vulnerability. Check on EVERY object access.

Key Points to Remember

  • 1Layer it: role gate → permission map → object ownership, each catching what the last misses
  • 2Check permissions ("drives:create"), not roles — policy changes touch one map
  • 3Scopes are per-token grants enforced via SecurityScopes/Security(); ideal for machine access
  • 4IDOR: valid users reading others' objects — ownership check on every access, return 404

Interview Questions

Sign in to ask Aria
1

Design authorization for a placement portal: students, TPOs, admins — roles or permissions, and where do checks live?

HardFreshworks
2

What is IDOR? Why does RBAC not prevent it, and why respond 404 instead of 403?

HardAmazon
3

When do OAuth2 scopes earn their complexity over role checks?

HardPostman

Ask Aria about RBAC & Scopes — Roles, Permissions, Ownership

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…