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.
Python code
44 linesfrom 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
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
- Use a dictionary mapping roles to permission flags for more granular checks
- 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
More from API design & gRPC
- Build a Bulk Array POST Mock Server in Python medium
- Build a Mock REST API with PUT and GET in Python medium
- Convert Protobuf to JSON and Dict in Python easy
- Create a Data Helper in Python for gRPC-style APIs easy
- Format data in Python using dataclasses like gRPC messages easy
- Generate an OpenAPI Spec from Mock Routes in Python easy
Keep learning
Related tutorials and quizzes for this topic.