Complete Guide to FastAPI RBAC Permission Control

📂 Stage: Stage 4 - Security and Authentication (Security) 🔗 Related chapters: FastAPI oauth2-jwt-auth · FastAPIdependency-injection

When you use FastAPI to build user authentication (such as JWT login), the next question that must be solved is: "What can this user do after logging in?" The answer is RBAC (Role-Based Access Control).

This article will take you from concept to code to implement a clear, decoupled, and scalable permission system. After reading you will master:

  • How to use enumerations to define permissions and roles, saying goodbye to confusing hard coding
  • How to use FastAPI's dependency injection to turn permission checking into a "declarative" decorator
  • How to achieve resource-level refined control such as "modify your own articles"
  • Performance and security practices that must be taken into account in production environments

When you're ready, let's get started.

Table of contents


Summary of core concepts

What is RBAC?

Simply put, RBAC separates "users" and "permissions" and inserts a layer of "roles" in the middle. Instead of directly labeling users with permissions, first define what permissions a role contains, and then assign roles to users.

For example: Alice is an administrator. She does not need to be given the "Delete User" permission separately. She only needs to be assigned the "Administrator" role, which has been bound with "Delete User" and other permissions.

graph LR
    User[Alice<br/>用户] --> Role[管理员<br/>角色] --> Perms[删除用户/修改配置<br/>权限]
    Perms --> Resource[用户列表/系统配置<br/>资源]
    Perms --> Action[DELETE/UPDATE<br/>操作]

The advantage of this design is: When the permission policy is adjusted, you only need to change the permission set in the role, instead of modifying it on each user.

Three-tier infrastructure

The entire RBAC model can be split into three layers:

  1. User layer: manages the relationship between users and roles (many-to-many: one user can have multiple roles)
  2. Role layer: manages the relationship between roles and permissions (many-to-many: one role contains multiple permissions)
  3. Permission layer: Define "what operations can be done on what resources", for examplearticle:update:ownIt means "you can only modify your own articles"

For the next code implementation, we will implement it according to this three-layer structure.


Minimalist permission model implemented

For most small and medium-sized projects, there is no need to build a bunch of database tables immediately. It can be run directly using Python enumeration + dictionary mapping, and it is also effortless to migrate to the database later. Next we define a set of static permissions and roles.

1. Define permission enumeration

I am used to naming permissions资源:操作[:范围]format, such asarticle:update:ownIt's clear at a glance.

# models/rbac_simple.py
from enum import Enum
from typing import Set, Dict, Optional
from dataclasses import dataclass
from fastapi import HTTPException, status

class Permission(str, Enum):
    # 用户管理
    USER_READ = "user:read"
    USER_CREATE = "user:create"
    USER_UPDATE = "user:update"
    USER_DELETE = "user:delete"

    # 内容管理
    ARTICLE_READ = "article:read"
    ARTICLE_CREATE = "article:create"
    ARTICLE_UPDATE_OWN = "article:update:own"
    ARTICLE_PUBLISH = "article:publish"

    # 系统/后台
    ADMIN_ACCESS = "admin:access"

2. Define role enumeration

class Role(str, Enum):
    SUPER_ADMIN = "super_admin"  # 超级管理员,拥有所有权限
    ADMIN = "admin"
    EDITOR = "editor"
    AUTHOR = "author"
    USER = "user"
    GUEST = "guest"

3. Establish role-permission mapping table

Directly use a dictionary to write down the permission set corresponding to each role, and it will be loaded at startup without the need for database query.

ROLE_PERMS: Dict[Role, Set[Permission]] = {
    Role.SUPER_ADMIN: set(Permission.__members__.values()),
    Role.ADMIN: {
        Permission.USER_READ, Permission.USER_UPDATE,
        Permission.ARTICLE_READ, Permission.ARTICLE_PUBLISH,
        Permission.ADMIN_ACCESS
    },
    Role.EDITOR: {
        Permission.ARTICLE_READ, Permission.ARTICLE_PUBLISH,
        Permission.ARTICLE_UPDATE_OWN  # 需要配合所有权检查
    },
    Role.AUTHOR: {
        Permission.ARTICLE_READ, Permission.ARTICLE_CREATE,
        Permission.ARTICLE_UPDATE_OWN
    },
    Role.USER: {Permission.ARTICLE_READ},
    Role.GUEST: {Permission.ARTICLE_READ}
}

This way we have a clean, readable permissions definition. Next, let's make these permissions actually protect our interface.


Dependency injection permission check

FastAPI dependency injection (Depends) is the best carrier to implement permission checking: you can write authorization logic as a reusable dependency, completely decoupled from business code.

Assuming we already have a JWT authentication dependencyget_current_user, it will return the currently logged inUserObject (containsrolefields andis_activestate).

Basic permission checker

We create two functions:require_any_permandrequire_role, they return a dependency that declares "what permissions or roles are required by this interface."

# dependencies/rbac_deps.py
from fastapi import Depends
from models.rbac_simple import Permission, Role, ROLE_PERMS
from models.user import User
from auth.jwt import get_current_user

# 检查是否拥有【任意一个】指定权限
def require_any_perm(*perms: Permission):
    async def checker(user: User = Depends(get_current_user)):
        # 1. 账号状态检查
        if not user.is_active:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="账户已被禁用"
            )
        # 2. 获取该用户当前角色的权限集
        user_perms = ROLE_PERMS.get(Role(user.role), set())
        # 3. 检查用户权限与需求权限是否有交集
        if not set(perms) & user_perms:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail=f"缺少权限:{[p.value for p in perms]}"
            )
        return user   # 可以将user传给路径操作函数
    return checker

# 检查是否拥有【某一个指定角色】
def require_role(*roles: Role):
    async def checker(user: User = Depends(get_current_user)):
        if not user.is_active:
            raise HTTPException(status.HTTP_401_UNAUTHORIZED, "账户已禁用")
        if Role(user.role) not in roles:
            raise HTTPException(
                status.HTTP_403_FORBIDDEN,
                detail=f"需要角色:{[r.value for r in roles]}"
            )
        return user
    return checker

Business interface usage example

By introducing these dependencies in path operations, permission logic is as clean as "tags".

# main.py
from fastapi import FastAPI, Depends
from models.rbac_simple import Permission, Role
from dependencies.rbac_deps import require_any_perm, require_role
from models.user import User

app = FastAPI(title="FastAPI RBAC Demo")

# 只有 admin 或 super_admin 才能访问后台
@app.get("/admin/dashboard", tags=["后台管理"])
async def admin_dashboard(
    _: User = Depends(require_role(Role.SUPER_ADMIN, Role.ADMIN))
):
    return {"message": "欢迎来到管理后台"}

# 拥有 ARTICLE_PUBLISH 权限的用户才能发布文章
@app.post("/articles/{article_id}/publish", tags=["内容管理"])
async def publish_article(
    article_id: int,
    _: User = Depends(require_any_perm(Permission.ARTICLE_PUBLISH))
):
    return {"message": f"文章 {article_id} 已发布"}

After doing this, the permission logic is completely abstracted, and the interface code only cares about the business itself.


Fine-grained resource-level control

The above permission check can only control "whether you can do something", but in real business, you often need to control "whether you can operate your own data". For example: authors can only modify their own articles, and editors can modify any article.

We can design a general resource ownership check decorator and use it together with FastAPI's dependency injection.

Resource ownership and permission combination check

Idea:

  • Check whether the user has the "operate all resources" permission (for example, the editor hasarticle:publishModification of ownership may be implied)
  • If not, check whether the user has the permission to "operate own resources" (for examplearticle:update:own
  • If there is neither, reject; if there is "own", then verify whether the resource owner matches the current user
# dependencies/rbac_deps.py (补充代码)
from typing import Callable, Any
from functools import wraps
from fastapi import Request

def require_resource_owner_or_perm(
    perm: Permission,                     # 操作自己资源所需的权限
    perm_for_all: Optional[Permission] = None,  # 操作所有资源所需的权限
    get_owner_func: Callable = None       # 获取资源所有者ID的异步函数
):
    def decorator(func: Callable):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            # 从路径操作函数的参数中获取当前用户(需要约定参数名)
            user: User = kwargs.get("current_user")
            if not user.is_active:
                raise HTTPException(status.HTTP_401_UNAUTHORIZED, "账户已禁用")

            # 超级管理员直接放行
            if Role(user.role) == Role.SUPER_ADMIN:
                return await func(*args, **kwargs)

            # 获取当前用户拥有的权限
            user_perms = ROLE_PERMS.get(Role(user.role), set())

            # 先检查是否能操作所有资源(更高权限)
            if perm_for_all and perm_for_all in user_perms:
                return await func(*args, **kwargs)

            # 检查是否有操作“自己的”资源的权限
            if perm not in user_perms:
                raise HTTPException(status.HTTP_403_FORBIDDEN, "缺少权限")

            # 获取资源ID并查询所有者
            resource_id = kwargs.get("article_id") or kwargs.get("resource_id")
            db = kwargs.get("db")
            owner_id = await get_owner_func(db, resource_id)

            if owner_id != user.id:
                raise HTTPException(status.HTTP_403_FORBIDDEN, "无权操作他人资源")

            return await func(*args, **kwargs)
        return wrapper
    return decorator

When using it, just add a decorator to the interface and pass in the real resource owner query function (for example, get thearticle.owner_id). In this way, complex rules such as "whose things belong to whom" can be clearly managed.


Performance and Security Best Practices

A working permission system is only the starting point. The production environment also needs to consider performance and security.

Performance optimization

  1. Permission Caching If you use a database to store permissions, be sure to introduce Redis cache user permissions (for example, cache for 24 hours). When roles or permissions are changed, the corresponding cache is actively deleted to avoid querying the database for every request.

  2. Preloading For statically defined permissions (such as the enumeration in this article), they are directly loaded into memory when the application starts to avoid repeated calculations during runtime.

  3. Minimum privilege query During dependency injection, only calculate the permissions required by the current interface. Do not pull all the user's permissions and perform a large number of set operations.

Security Best Practices

  • Default Denied: All endpoints are inaccessible by default, and only interfaces with explicit dependencies are open.
  • Principle of Least Permission: Assign users the least permissions that can complete their work, rather than giving them all to save trouble.
  • Audit Log: All role assignments, permission changes, and sensitive operations (deleting users, exporting data) should be logged to facilitate traceability.
  • Second confirmation for sensitive operations: Operations such as deleting resources, modifying system configurations, etc. require re-verification (such as entering a password or verification code).
  • Permission changes take effect immediately: After modifying roles or permissions, be sure to invalidate the session or cache of the relevant user immediately to prevent the old permissions from continuing to be available.

Summarize

This article takes you through using FastAPI to implement a complete RBAC system from static definition to fine-grained resource control:

  1. usePermissionRoleEnumeration clearly defines permission space
  2. passROLE_PERMSDictionary to establish role-permission mapping
  3. useDependsBuild reusable permission checking dependencies (require_any_permrequire_role
  4. Use decorators to achieve fine-grained control of "you can only operate your own resources"
  5. Introduce production-level practices such as caching, default rejection, and audit logs

This solution is completely sufficient for small and medium-sized projects, with zero code coupling. If the project expands, the static mapping can be smoothly upgraded to database dynamic permission storage, and the core checking logic requires almost no modification.

Now, you can apply the same idea to your own FastAPI project to make permission management clear, secure, and maintainable. If you find it helpful, please share it with friends who need it!