How to Implement RBAC Permission Checks with a Route Decorator in Python

Build a reusable Python decorator that checks a user's role against allowed roles and raises a custom PermissionError when access is denied.

Easy Python 3.9+ Aug 9, 2026 API design & gRPC 13 views 0 copies

Python code

44 lines
Python 3.9+
from functools import wraps
from enum import Enum

class Role(Enum):
    ADMIN = "admin"
    MODERATOR = "moderator"
    USER = "user"

class PermissionError(Exception):
    pass

def require_role(*allowed_roles):
    def decorator(func):
        @wraps(func)
        def wrapper(user_role, *args, **kwargs):
            if user_role not in allowed_roles:
                raise PermissionError(f"Access denied for role: {user_role}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@require_role(Role.ADMIN, Role.MODERATOR)
def delete_post(post_id):
    return f"Post {post_id} deleted successfully"

@require_role(Role.ADMIN)
def ban_user(username):
    return f"User {username} banned successfully"

if __name__ == "__main__":
    # Demo with different roles
    test_cases = [
        (Role.ADMIN, delete_post, (101,)),
        (Role.USER, delete_post, (101,)),
        (Role.MODERATOR, delete_post, (102,)),
        (Role.MODERATOR, ban_user, ("john_doe",)),
    ]
    
    for role, func, args in test_cases:
        try:
            result = func(role, *args)
            print(f"{role.value}: {result}")
        except PermissionError as e:
            print(f"{role.value}: {e}")

Output

stdout
admin: Post 101 deleted successfully
user: Access denied for role: Role.USER
moderator: Post 102 deleted successfully
moderator: Access denied for role: Role.MODERATOR

How it works

The require_role decorator factory accepts allowed roles and returns a decorator that wraps the route function. Inside the wrapper, the first argument user_role is checked against the allowed roles; if it's not allowed, a PermissionError is raised. Using functools.wraps preserves the original function's metadata for debugging. Role constants come from an Enum, which avoids typos and makes the code self-documenting.

Common mistakes

  • Forgetting to pass the user_role as the first argument to the decorated function
  • Using strings instead of the Role enum, leading to inconsistent comparisons
  • Not using @wraps, which breaks introspection and stack traces
  • Allowing None or missing roles to pass without explicit checks

Variations

  1. Use a dictionary mapping roles to permission flags for more granular checks
  2. Integrate with a framework like Flask by inspecting `request.user.role` inside the decorator

Real-world use cases

  • Protecting admin endpoints in a REST API where certain routes are only accessible to moderators or administrators
  • Enforcing role-based access in GraphQL resolvers or gRPC methods based on the authenticated user's role
  • Building a microservice that exposes internal endpoints with limited permissions to service accounts

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from API design & gRPC

Related tutorials and quizzes for this topic.