Web Dev

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.

FFarjadCEO & AI Engineer
7 min read
R

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:resource pairs.
  • 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.

All articlesWritten by Farjad · CEO & AI Engineer

Have a project that needs this kind of engineering?

We turn ideas into production software — AI, web, mobile, and cloud.

Start a Conversation