FastAPI’s role-based access control (RBAC) isn’t just about checking a user’s role; it’s fundamentally about defining what actions a user can perform based on their assigned roles, thereby enforcing granular security policies.
Let’s see this in action. Imagine a simple API for managing blog posts, where only 'admin' users can delete posts, while 'editor' and 'admin' can create and update them.
from fastapi import FastAPI, Depends, HTTPException, status
from pydantic import BaseModel
from typing import List, Optional
app = FastAPI()
# --- Dummy Data and Models ---
class Post(BaseModel):
id: int
title: str
content: str
author_id: int
class User(BaseModel):
id: int
username: str
roles: List[str] # e.g., ["user", "editor", "admin"]
# In-memory "database"
posts_db = {
1: Post(id=1, title="First Post", content="Content of first post", author_id=1),
2: Post(id=2, title="Second Post", content="Content of second post", author_id=2),
}
users_db = {
1: User(id=1, username="alice", roles=["user"]),
2: User(id=2, username="bob", roles=["editor"]),
3: User(id=3, username="charlie", roles=["admin"]),
}
# --- Authentication/Authorization Utilities ---
# This is a simplified way to get the current user.
# In a real app, this would involve tokens, sessions, etc.
def get_current_user(user_id: int = 1) -> User:
user = users_db.get(user_id)
if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
return user
# --- RBAC Dependency ---
def role_required(required_roles: List[str]):
def _role_required(current_user: User = Depends(get_current_user)):
if not any(role in current_user.roles for role in required_roles):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"You do not have the required roles: {', '.join(required_roles)}",
)
return current_user
return _role_required
# --- API Endpoints ---
@app.get("/posts", response_model=List[Post])
def read_posts(current_user: User = Depends(get_current_user)):
# Any authenticated user can read posts
return list(posts_db.values())
@app.post("/posts", response_model=Post)
def create_post(
post: Post,
current_user: User = Depends(role_required(["editor", "admin"]))
):
# Editors and Admins can create posts
if post.id in posts_db:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Post ID already exists")
posts_db[post.id] = post
return post
@app.put("/posts/{post_id}", response_model=Post)
def update_post(
post_id: int,
updated_post: Post,
current_user: User = Depends(role_required(["editor", "admin"]))
):
# Editors and Admins can update posts
if post_id not in posts_db:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Post not found")
posts_db[post_id] = updated_post
return updated_post
@app.delete("/posts/{post_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_post(
post_id: int,
current_user: User = Depends(role_required(["admin"]))
):
# Only Admins can delete posts
if post_id not in posts_db:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Post not found")
del posts_db[post_id]
return
# --- Example Usage (simulated with different user_ids) ---
# To test:
# 1. Run this script with `uvicorn your_script_name:app --reload`
# 2. Use a tool like curl or Postman.
#
# Example curl commands (assuming running on http://127.0.0.1:8000):
#
# GET /posts (any user)
# curl http://127.0.0.1:8000/posts
#
# POST /posts (as user_id=2, 'editor')
# curl -X POST http://127.0.0.1:8000/posts -H "Content-Type: application/json" -d '{"id": 3, "title": "New Post", "content": "This is new", "author_id": 2}'
#
# POST /posts (as user_id=1, 'user' - should fail with 403)
# curl -X POST http://127.0.0.1:8000/posts -H "Content-Type: application/json" -d '{"id": 4, "title": "Another New Post", "content": "This is new", "author_id": 1}'
#
# DELETE /posts/1 (as user_id=3, 'admin')
# curl -X DELETE http://127.0.0.1:8000/posts/1
#
# DELETE /posts/2 (as user_id=2, 'editor' - should fail with 403)
# curl -X DELETE http://127.0.0.1:8000/posts/2
This example uses FastAPI’s dependency injection system to enforce RBAC. The role_required function is a factory that returns a dependency. This dependency, when injected into an endpoint, first fetches the current_user (via get_current_user) and then checks if the user’s roles intersect with the required_roles list passed to the factory. If the roles don’t match, a 403 Forbidden error is raised.
The core problem RBAC solves is managing permissions at scale. Instead of assigning specific permissions to individual users (which quickly becomes unmanageable), you assign permissions to roles (like 'admin', 'editor', 'viewer'), and then assign roles to users. This makes it easy to grant or revoke broad sets of permissions by simply changing a user’s role assignment.
Internally, FastAPI’s dependency system is key. Dependencies are functions that can be "plugged in" to your API endpoints. They run before your endpoint’s main logic. This makes them ideal for tasks like authentication and authorization. Our role_required dependency acts as a gatekeeper. It receives the authenticated user object and verifies their authorization before allowing the request to proceed to the endpoint’s handler.
The get_current_user function is a placeholder for your actual authentication mechanism. In a real application, this would likely involve checking a JWT token, a session cookie, or an API key to identify the user and retrieve their associated roles. The users_db and posts_db are also in-memory examples; these would typically be database lookups.
When you define an endpoint like create_post, you use Depends(role_required(["editor", "admin"])). This tells FastAPI: "Before executing create_post, run the role_required dependency, ensuring the user has either the 'editor' or 'admin' role." If the user doesn’t have one of these roles, the HTTPException is raised, and the create_post function itself is never called.
The most surprising aspect of implementing RBAC this way is how cleanly it separates concerns. Your endpoint logic focuses solely on the business task (creating a post), while the Depends clause handles the security check. This leads to much more readable and maintainable code. You can change authorization rules by modifying the role_required calls or the role_required factory itself, without cluttering your core business logic with permission checks.
The next step is to integrate this with a robust authentication system, likely involving external libraries for JWT handling or OAuth2.