Skip to content

Role-Based Access Control (RBAC)

🎯 What You’ll Learn

  • What RBAC is and why it matters
  • How to store and check user roles in your database
  • How to protect routes using FastAPI’s Depends system
  • How to enforce admin-only access with reusable dependencies

🧠 What Is Role-Based Access Control?

RBAC is a security pattern where access to resources is granted based on a user’s role.

For example:

  • ✅ Regular users can read and update their own data
  • 🔒 Admins can manage all users, delete data, or access analytics

RBAC helps you:

  • Enforce least privilege
  • Prevent unauthorized actions
  • Keep your API secure and maintainable

🧱 Step 1: Add a Role Field to the User Model

📄 models/user.py

from sqlmodel import SQLModel, Field
from typing import Optional

class User(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    username: str = Field(index=True, unique=True)
    email: str
    hashed_password: str
    is_active: bool = True
    role: str = "user"  # "user" or "admin"

🔍 Why a string?

  • Simple to store and query
  • Easy to extend later (e.g. “editor”, “moderator”)

🔐 Step 2: Update Signup to Assign Roles

📄 routers/auth.py

@router.post("/signup", status_code=201)
def signup(user: User, session: Session = Depends(get_session)):
    ...
    user.role = user.role or "user"  # default to "user"
    ...

You can later create an admin manually or via a protected endpoint.


🧩 Step 3: Get the Current User with Role

📄 core/dependencies.py

from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
from core.jwt import decode_access_token
from sqlmodel import Session, select
from db import get_session
from models.user import User

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")

def get_current_user(token: str = Depends(oauth2_scheme), session: Session = Depends(get_session)) -> User:
    payload = decode_access_token(token)
    if not payload or "sub" not in payload:
        raise HTTPException(status_code=401, detail="Invalid token")

    user = session.exec(select(User).where(User.username == payload["sub"])).first()
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return user

This returns the full User object, including their role.


🛡️ Step 4: Create Role-Based Dependencies

📄 core/dependencies.py (continued)

def require_admin(user: User = Depends(get_current_user)):
    if user.role != "admin":
        raise HTTPException(status_code=403, detail="Admin access required")
    return user

def require_active_user(user: User = Depends(get_current_user)):
    if not user.is_active:
        raise HTTPException(status_code=403, detail="Inactive account")
    return user

These can be reused across any route to enforce access control.


🔐 Step 5: Protect Routes with Role Checks

📄 routers/users.py

from core.dependencies import require_admin

@router.get("/admin/users", dependencies=[Depends(require_admin)])
def list_all_users(session: Session = Depends(get_session)):
    return session.exec(select(User)).all()

📄 routers/tasks.py

from core.dependencies import require_active_user

@router.get("/", dependencies=[Depends(require_active_user)])
def get_tasks(...):
    ...

🧪 Try It in Swagger UI

  1. Log in as a regular user → try /admin/users → get 403
  2. Log in as an admin → try /admin/users → get full list

FastAPI automatically documents required tokens and shows 403 errors for unauthorized access.


🧠 Recap

You now have:

Role Permissions
user Access own data, basic features
admin Access all users, manage system
Component Purpose
role field Stores user role
require_admin() Enforces admin-only access
require_active_user() Blocks inactive accounts

🧪 Practice Challenge

Add:

  • A PATCH /users/{id}/role endpoint (admin-only) to promote users
  • A GET /users/me endpoint to return the current user’s profile
  • A GET /users/stats endpoint (admin-only) to count users by role