Role-Based Access Control in Python — Core Concepts
What RBAC solves
Authorization answers “what can this user do?” Authentication tells you who someone is; authorization determines what they’re allowed to do. Without a structured model, authorization logic scatters across the codebase as ad-hoc if user.id == 42 checks that become unmaintainable.
RBAC provides that structure. The core idea: permissions attach to roles, roles attach to users. This indirection layer means you manage permissions at the role level, not the individual user level.
The RBAC model
Three building blocks:
Users — the people (or service accounts) using the system.
Roles — named collections of permissions. Examples: viewer, editor, admin, billing_manager.
Permissions — specific actions on specific resources. Examples: posts:read, posts:write, users:delete, billing:export.
The relationship is: User → has Roles → which grant Permissions.
Role hierarchy
Roles can inherit from each other. An admin role might inherit all editor permissions, which in turn inherits all viewer permissions. This avoids duplicating permission assignments:
viewer → posts:read, comments:read
editor → (inherits viewer) + posts:write, comments:write
admin → (inherits editor) + users:manage, settings:edit
How it works in practice
A typical RBAC check follows this flow:
- User authenticates (login, JWT, session)
- Application loads the user’s roles (from database, token claims, or cache)
- Application determines what permission is needed for the requested action
- Application checks if any of the user’s roles grant that permission
- Allow or deny
class RBACChecker:
def __init__(self):
self.role_permissions = {
"viewer": {"posts:read", "comments:read"},
"editor": {"posts:read", "posts:write", "comments:read", "comments:write"},
"admin": {"posts:read", "posts:write", "posts:delete",
"comments:read", "comments:write", "comments:delete",
"users:manage"},
}
def has_permission(self, user_roles: list[str], permission: str) -> bool:
for role in user_roles:
if permission in self.role_permissions.get(role, set()):
return True
return False
Database schema
The standard RBAC schema uses three tables plus two join tables:
users — id, email, name
roles — id, name, description
permissions — id, name (like posts:write)
user_roles — user_id, role_id (many-to-many)
role_permissions — role_id, permission_id (many-to-many)
This design lets you query a user’s effective permissions with two joins, and makes it easy to add or remove permissions from roles without touching individual users.
RBAC in web frameworks
Most Python web frameworks have RBAC patterns:
Django — built-in Group model maps to roles, Permission model is auto-generated from models. The @permission_required decorator handles view-level checks.
Flask — Flask-Principal or Flask-Security add role and permission management. Custom decorators are common.
FastAPI — dependency injection makes RBAC clean. Create a dependency that checks permissions and inject it into route definitions.
Common misconception
“RBAC is enough for all authorization needs.” RBAC works well when permissions depend on the user’s job function. It struggles when permissions depend on the data itself — for example, “editors can only edit posts in their department” or “users can only see their own orders.” Those data-dependent rules need attribute-based access control (ABAC) or ownership checks layered on top of RBAC.
The one thing to remember: RBAC creates a clean abstraction layer — permissions live on roles, roles live on users — so authorization scales from 10 users to 10,000 without individual permission management becoming a nightmare.
See Also
- Python Api Key Management Why apps use special passwords called API keys, and how to keep them safe — explained with a library card analogy
- Python Attribute Based Access Control How apps make fine-grained permission decisions based on who you are, what you're accessing, and the circumstances — explained with an airport analogy
- Python Audit Logging Learn Audit Logging with a clear mental model so your Python code is easier to trust and maintain.
- Python Bandit Security Scanning Why Bandit Security Scanning helps Python teams catch painful mistakes early without slowing daily development.
- Python Clickjacking Prevention How invisible website layers trick you into clicking the wrong thing, and how Python apps stop it