Role Management

RBAC, ABAC, and ReBAC in FastAPI: Implementation Patterns and Enterprise Governance

July 9, 2026
8 MIn read
About the author

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Access control in FastAPI follows the same conceptual progression as access control everywhere else: RBAC is simple, ABAC is contextual, and ReBAC is where things get genuinely complex. Each model exists because it solves a problem the previous one cannot.

This guide covers practical FastAPI implementation patterns for all three, when to use each, how to avoid overcomplicating your application code, and what the enterprise access management layer above these looks like once your application is in production.

The Three Models and When Each One Applies

RBAC (Role-Based Access Control) grants access based on a user's assigned role. An admin can delete users. An editor can update content. A viewer can only read. The role is a stable property of the user, and the permission check is a simple membership test.

RBAC handles the majority of access control scenarios cleanly. It is easy to reason about, easy to audit, and easy to implement. The limitation is that it does not account for context beyond the user's role.

ABAC (Attribute-Based Access Control) evaluates access based on attributes of the user, the resource, and the environment. Not just "is this user an HR manager?" but "is this HR manager in the UK, with clearance level 3 or above, accessing a confidential document during business hours?" The access decision is a policy evaluated against multiple attributes simultaneously.

ABAC is appropriate when role-based checks are too coarse. A doctor can write prescriptions, but only for patients they are actively treating. That second constraint is an attribute relationship, not a role check.

ReBAC (Relationship-Based Access Control) evaluates access based on the relationship graph between the requesting user and the resource. A user can view a post because they are friends with the author, who owns the post, which is in a collection the user has been granted access to. The access decision traverses a graph of relationships rather than checking static properties.

ReBAC is appropriate when access follows ownership and sharing semantics: document collaboration, social media visibility, organizational hierarchy-based access.

In practice, most applications use a combination. RBAC for coarse-grained role checks, ABAC for attribute-driven contextual decisions, and ReBAC for resource-ownership relationships.

RBAC in FastAPI: The Dependency Injection Pattern

FastAPI's dependency injection system is the cleanest way to implement access control. Middleware lacks endpoint-specific context. Hardcoding checks in every route function does not scale. A parameterized dependency covers both problems.

from fastapi import Depends, HTTPException, status
from typing import Annotated
def RoleChecker(allowed_roles: list[str]):
    def check_role(current_user: User = Depends(get_current_user)):
        if current_user.role not in allowed_roles:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="You do not have the required role."
            )
        return current_user
    return check_role
# Usage
@router.delete("/users/{user_id}")
def delete_user(
    user_id: int,
    user: User = Depends(RoleChecker(["admin"]))
):
    return {"message": "User deleted"}

The role list is passed at the route level, which keeps the access requirement visible at the point of definition. The dependency raises a 403 if the user's role is not in the allowed list.

For protecting an entire router rather than individual routes, pass the dependency at the router level:

admin_router = APIRouter(dependencies=[Depends(RoleChecker(["admin"]))])

One practical consideration: roles stored as a string field ("admin editor") or as a list in the JWT token work for basic cases. As roles multiply and assignments become more dynamic, managing them in a database table and resolving them at authentication time scales better.

ABAC in FastAPI: Contextual Attribute Checks

ABAC dependencies are more specific because the access decision depends on context that varies per endpoint. The cleanest pattern is a named dependency that encapsulates the specific policy:

def hr_clearance_checker(current_user: User = Depends(get_current_user)):
    if current_user.department != "HR" or current_user.clearance_level < 3:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Insufficient clearance for this resource."
        )
    return current_user
@router.get("/documents/confidential")
def get_confidential_files(user: User = Depends(hr_clearance_checker)):
    return {"data": "Confidential files"}

For more generalized ABAC, a policy evaluation function that takes a set of attributes and a policy definition keeps the logic centralized:

def AttributeChecker(required_attributes: dict):
    def check_attributes(current_user: User = Depends(get_current_user)):
        for attr, value in required_attributes.items():
            if getattr(current_user, attr, None) != value:
                raise HTTPException(status_code=403, detail="Access denied.")
        return current_user
    return check_attributes
@router.get("/documents/confidential")
def get_confidential_files(
    user: User = Depends(AttributeChecker({"department": "HR", "clearance_level": 3}))
):
    return {"data": "Confidential files"}

The generalized version is cleaner for simple attribute checks. For complex ABAC policies with logical combinations (AND, OR, NOT across multiple attributes), an external policy engine (OPA, Casbin) avoids encoding complex decision trees in Python code.

ReBAC in FastAPI with SQLAlchemy: Let the Database Do the Work

ReBAC relationship checks in Python code are slow and hard to reason about when relationships span multiple hops. The right pattern is to push the relationship check into the database query: fetch the resource only if the relationship exists.

from sqlalchemy.orm import Session
def get_accessible_post(
    post_id: int,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user)
):
    # Only fetch the post if the current user is friends with the author
    post = (
        db.query(Post)
        .join(Friendship, Friendship.friend_id == Post.author_id)
        .filter(
            Post.id == post_id,
            Friendship.user_id == current_user.id
        )
        .first()
    )
    if not post:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Post not found or access denied."
        )
    return post
@router.get("/posts/{post_id}")
def view_post(post: Post = Depends(get_accessible_post)):
    return post

Returning a 404 rather than a 403 for inaccessible resources is a deliberate choice in ReBAC scenarios: it avoids leaking information about whether the resource exists when the requester should not know about it.

The SQLAlchemy join approach works well for one or two relationship hops. For deeper graphs (user is in a group, which is a subgroup of an organization, which has access to a folder, which contains the document), the join complexity becomes unmanageable. At that point, an external authorization service built on the Zanzibar paper (SpiceDB, Ory Keto, OpenFGA) handles the graph traversal more efficiently and with better performance characteristics.

The Architectural Question: Where Should Authorization Live?

Several practitioners in this space make the point that authorization is an architectural problem, not a FastAPI problem. The framework provides the mechanism (dependency injection, middleware) but the business logic of who can do what to which resource belongs to your application's domain, not to the web framework.

For simple applications, embedding authorization logic as FastAPI dependencies works well. As applications grow, the pattern of pushing authorization to a separate service becomes worth considering: a dedicated authorization microservice that accepts (user, action, resource) and returns an allow/deny decision. The application code calls the authorization service rather than evaluating policies inline.

This separation has a practical advantage: authorization policy changes do not require application code changes. The policy lives in the authorization service and can be updated independently.

The emerging standard for this separation is the AuthZen framework, which standardizes the API contract between applications and authorization services: "Can user X perform action Y on resource Z?" Any application, regardless of language or framework, can ask this question in a standard format.

Managing RBAC and ABAC at Enterprise Scale

The implementation patterns above handle the enforcement layer: checking whether the current user can perform the requested action. In enterprise environments, there is a separate governance problem: managing who has which roles, which attributes, and which relationship memberships across a growing user base and a growing application portfolio.

When an employee joins and needs the "editor" role in your FastAPI application, someone has to assign it. When they change departments, someone has to update their department attribute. When they leave, every role and attribute assignment across every application needs to be revoked.

IGA platforms handle this lifecycle above the application layer. Zluri, for example, connects to your HRMS and uses employee attributes (department, location, job title) to automatically assign roles and permissions based on defined rules. When HR marks an employee as having moved to the HR department, an automation rule triggers that grants them the appropriate clearance attributes in connected applications. When they leave, all access is revoked across all connected applications as part of the offboarding playbook.

For the application-level RBAC and ABAC discussion: rather than a database administrator manually updating user role tables, the IGA platform drives those changes through the application's API or directory integration. The application enforces the policy; the IGA platform manages who is subject to which policy.

Zluri's product roadmap also includes AuthZen framework integration, which would eventually allow connected applications to delegate authorization decisions to the platform's policy engine rather than maintaining policy logic in application code. The application asks "can this user do this?" and the governance platform answers based on current role and attribute state, removing the need to synchronize role data to the application's own database.

Practical Guidance for Choosing Your Pattern

Start with RBAC. It covers most access control scenarios and is simple to implement and audit. Add ABAC dependencies for endpoints that need contextual decisions beyond role membership. Implement ReBAC at the database query layer for resource ownership scenarios, and consider an external authorization service when relationship graphs exceed two or three levels of depth.

Keep authorization logic in dependencies rather than middleware, since middleware lacks the endpoint-specific context that makes access decisions meaningful. Use Annotated types in FastAPI for cleaner dependency injection signatures.

As the application and organization grow, the effort to maintain role and attribute assignments manually will eventually exceed the effort to connect to an IGA platform that handles lifecycle management automatically.