RBAC Done Right: 4 Roles, 16 Permissions, Zero Confusion
A pragmatic role-based access control (RBAC) schema you can ship this week — roles, granular permissions, and the data model that keeps authorization sane.
Most authorization bugs are not exotic — they are the result of roles and permissions getting tangled until nobody can say who can do what. Role-Based Access Control (RBAC) fixes that, but only if you model it deliberately. Here is the compact schema we deploy: four roles, sixteen permissions, and one rule that prevents the usual mess.
Roles are not permissions
The single most important rule: never check roles in your business logic — check permissions. Roles are bundles of permissions that exist for human convenience. Code should ask “can this user delete:invoice?”, not “is this user an admin?”. When requirements change, you re-map permissions to roles instead of hunting through the codebase. This mirrors the principle of least privilege.
The four roles
- Owner — full control, including billing and member management.
- Admin — manages content and users, but not billing.
- Member — creates and edits their own resources.
- Viewer — read-only access across the workspace.
Permissions as verb:noun pairs
Name every permission as action:resource. It reads like English and scales predictably:
PERMISSIONS = {
"Owner": ["*:*"],
"Admin": ["create:user", "read:user", "update:user", "delete:user",
"create:project", "read:project", "update:project", "delete:project"],
"Member": ["create:project", "read:project", "update:project", "read:user"],
"Viewer": ["read:project", "read:user"],
}Enforcing it once
Centralize the check in a single dependency or middleware so individual endpoints stay declarative.
def require(permission: str):
def checker(user: User = Depends(current_user)):
if not user.can(permission):
raise HTTPException(403, "Forbidden")
return user
return checker
@app.delete("/projects/{id}")
def delete_project(id: int, user: User = Depends(require("delete:project"))):
...Common pitfalls
- Hard-coding
if role == "admin"checks — the exact thing RBAC exists to prevent. - Forgetting resource ownership — a Member should edit their project, not everyone’s. Combine RBAC with a simple ownership check.
- No audit trail. Log every denied request; it is your earliest signal of both bugs and intrusions.
Key takeaways
- Check permissions, never roles, in application code.
- Model permissions as
action:resourcepairs. - Enforce in one place; layer ownership on top of RBAC.
Authorization is foundational to every product on our development services. If you want a security review or a clean RBAC rebuild, get in touch.