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
Dependssystem - 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¶
- Log in as a regular user → try
/admin/users→ get 403 - 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}/roleendpoint (admin-only) to promote users - A
GET /users/meendpoint to return the current user’s profile - A
GET /users/statsendpoint (admin-only) to count users by role