Role-Based Access Control (RBAC)
RBAC is a common pattern: users have roles (admin, editor, viewer), roles have permissions (delete:post, edit:post, view:post). To check if a user can perform an action, look up their role and check if the role has that permission. Simple, scalable, and works for most apps.
4 min read
What is RBAC?
RBAC is a way to organize permissions into roles and assign roles to users.
User: alice
Roles: [Editor]
Permissions: [read:post, create:post, edit:own_post, delete:own_post]
User: bob
Roles: [Viewer]
Permissions: [read:post]
User: charlie
Roles: [Admin]
Permissions: [read:post, create:post, edit:any_post, delete:any_post, manage_users]
When alice tries to delete a post, you check:
- Is alice authenticated? Yes.
- What roles does alice have? [Editor].
- Does [Editor] have permission delete:post? No (only delete:own_post).
- Is alice the owner of this post? Yes.
- Does [Editor] have permission delete:own_post? Yes.
- Allow the delete.
RBAC data model
class User:
id: str
email: str
roles: list[str] # ["editor", "viewer"]
class Role:
id: str
name: str
permissions: list[str] # ["read:post", "edit:post"]
# In your database:
users table: id, email, hashed_password, created_at
user_roles table: user_id, role_id
roles table: id, name
role_permissions table: role_id, permission_id
permissions table: id, name, descriptionOr simplified (for small apps):
class User:
id: str
email: str
role: str # "admin" | "editor" | "viewer"
def get_permissions(role: str) -> list[str]:
return {
"admin": ["read:post", "create:post", "edit:post", "delete:post"],
"editor": ["read:post", "create:post", "edit:own_post", "delete:own_post"],
"viewer": ["read:post"],
}.get(role, [])Authorization check at action time
When a user tries to perform an action, check their role/permissions:
def can_delete_post(user: User, post_id: str) -> bool:
post = db.get_post(post_id)
if not post:
return False # post doesn't exist
# Get user's permissions
permissions = get_permissions(user.role)
# Check "delete:post"
if "delete:post" in permissions:
return True # admins can delete any post
# Check "delete:own_post"
if "delete:own_post" in permissions:
return user.id == post.owner_id # can delete if you own it
return False # no delete permission
@app.delete('/posts/<post_id>')
def delete_post(post_id):
if not can_delete_post(request.user, post_id):
abort(403)
db.delete_post(post_id)
return 200Middleware approach (Django example)
Define a decorator to check permissions:
def requires_permission(permission: str):
def decorator(view_func):
def wrapper(request, *args, **kwargs):
user = request.user
if not user.is_authenticated:
return 401
permissions = get_permissions(user.role)
if permission not in permissions:
return 403
return view_func(request, *args, **kwargs)
return wrapper
return decorator
@app.delete('/posts/<post_id>')
@requires_permission("delete:post")
def delete_post_admin_only(post_id):
db.delete_post(post_id)
return 200
@app.delete('/posts/<post_id>')
def delete_post(post_id):
if not can_delete_post(request.user, post_id):
abort(403)
db.delete_post(post_id)
return 200Hierarchical roles
In some systems, roles inherit permissions:
def get_permissions(role: str) -> set[str]:
permissions = set()
if role in ["admin", "moderator", "editor", "viewer"]:
permissions.add("read:post")
if role in ["admin", "moderator", "editor"]:
permissions.update(["create:post", "edit:own_post"])
if role in ["admin", "moderator"]:
permissions.update(["edit:any_post", "delete:any_post"])
if role == "admin":
permissions.add("manage_users")
return permissionsRBAC limitations and when to use ABAC
RBAC works well for simple systems with a handful of roles. As the system grows, it can become unwieldy:
Problem: Alice is an editor, but only for her own blog. Bob is an editor for company events. Charlie is an editor for company news but also for events.
You'd end up with many fine-grained roles: editor-blog-personal, editor-events-company, editor-news-company, etc.
Solution: ABAC (Attribute-Based Access Control) makes decisions based on attributes:
def can_edit_post(user: User, post_id: str) -> bool:
post = db.get_post(post_id)
# Check multiple attributes
if user.role == "admin":
return True
if user.role == "editor" and user.id == post.owner_id:
return True
if user.department == "news" and post.category == "news":
return True
return FalseABAC is more flexible but also more complex. Most apps start with RBAC and add ABAC when they outgrow it.
Multi-tenant RBAC
In multi-tenant apps, permissions are scoped by organization:
class User:
id: str
email: str
class UserOrganization:
user_id: str
organization_id: str
role: str # "admin", "editor", "viewer" (within this org)
def can_delete_post(user: User, org_id: str, post_id: str) -> bool:
# Get user's role in this organization
user_org = db.get_user_organization(user.id, org_id)
if not user_org:
return False # user doesn't belong to this org
permissions = get_permissions(user_org.role)
# Check permissions within this org
post = db.get_post(post_id)
if post.organization_id != org_id:
return False # post belongs to a different org
if "delete:post" in permissions:
return True
if "delete:own_post" in permissions and user.id == post.owner_id:
return True
return FalseThe key: users can have different roles in different organizations. Alice is an admin in Company A but a viewer in Company B.
Common mistakes
Trusting the frontend
// WRONG — frontend hides button for non-admins
if (user.role === "admin") {
showDeleteButton();
}The frontend is untrusted. A user can open DevTools and make the button appear. Authorization must be enforced on the backend.
Not checking organization membership
# WRONG — doesn't check if user belongs to this org
post = db.get_post(post_id)
if user.role == "editor":
db.delete_post(post_id) # allows editing posts from other orgsCORRECT
post = db.get_post(post_id)
user_org = db.get_user_organization(user.id, post.organization_id)
if user_org and user_org.role == "editor":
db.delete_post(post_id)Hardcoding permissions in the view
# WRONG — permissions are hardcoded; hard to change
@app.delete('/posts/<post_id>')
def delete_post(post_id):
if request.user.role != "admin":
abort(403)
db.delete_post(post_id)CORRECT
def can_perform(user: User, action: str, resource_id: str) -> bool:
# Permission logic is centralized and testable
...
@app.delete('/posts/<post_id>')
def delete_post(post_id):
if not can_perform(request.user, "delete", post_id):
abort(403)
db.delete_post(post_id)Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. In RBAC, how do you determine if a user can perform an action?
2. What is the difference between delete:post and delete:own_post permissions?
3. In a multi-tenant app, what must you check when a user tries to access a resource?
4. When should you move from RBAC to ABAC?