RBAC vs ABAC: How to Design Roles and Permissions in an App
Practice4 minInnovialabs editorial team
In short
Start with RBAC — roles that bundle named permissions — and add ABAC-style rules only where access depends on data, such as "own deals" or "deals of my branch", always checking both on the server.
The short answer
- RBAC (role-based access control): a user gets roles, a role gets permissions. "Manager can edit deals." Simple to understand and audit.
- ABAC (attribute-based access control): a decision is made from attributes of the user, the resource and the context. "A manager can edit a deal if it belongs to their branch and is not closed."
Most real apps use a hybrid: RBAC answers "can this person do this type of action at all", attribute rules answer "on which records".
How they compare
| RBAC | ABAC | |
|---|---|---|
| Question it answers | What actions does a role allow? | Is this action allowed on this object now? |
| Example in CRM | Sales manager can view deals | Manager sees only deals of their branch |
| Setup effort | Low | Higher: rules need design and testing |
| Auditing | Easy: list roles and permissions | Harder: you must read the rules |
| Typical risk | Role explosion | Rules nobody understands |
Model permissions, not roles
The most common mistake is checking roles in code: if (user.role === "admin"). When a new role appears, you must hunt for every such check. Instead, check permissions and let roles be just bundles of them.
CREATE TABLE permissions (
id serial PRIMARY KEY,
code text UNIQUE NOT NULL -- e.g. 'deals.view', 'deals.edit', 'users.manage'
);
CREATE TABLE roles (
id serial PRIMARY KEY,
name text UNIQUE NOT NULL
);
CREATE TABLE role_permissions (
role_id int REFERENCES roles(id),
permission_id int REFERENCES permissions(id),
PRIMARY KEY (role_id, permission_id)
);
CREATE TABLE user_roles (
user_id int REFERENCES users(id),
role_id int REFERENCES roles(id),
PRIMARY KEY (user_id, role_id)
);
Permission codes live in code as constants; roles live in the database and can be edited in an admin panel without a release.
Add attributes where data matters
Scope rules usually depend on a few attributes: owner, team, branch, record status. Put them in one policy function per resource:
function canEditDeal(user: User, deal: Deal): boolean {
if (!user.permissions.has("deals.edit")) return false;
if (deal.status === "closed") return user.permissions.has("deals.edit_closed");
if (user.permissions.has("deals.edit_any")) return true;
if (deal.ownerId === user.id) return true;
return user.permissions.has("deals.edit_branch") && deal.branchId === user.branchId;
}
For lists, apply the same rule as a database filter, not by loading everything and hiding rows in the UI. Otherwise the API still returns other branches' data, and pagination counts are wrong.
Enforce on the server
- The UI may hide buttons, but every API endpoint checks permissions itself. Requests can be sent directly, bypassing the interface.
- Check access to the specific object, not just the action. Changing
/deals/42to/deals/43in a request must not open someone else's deal — this is the classic IDOR / broken object level authorization. - Deny by default: a new endpoint without a policy should return 403, not work for everyone.
- Log permission changes and role assignments as security events.
How to avoid role explosion
Role explosion happens when every exception becomes a new role: "Manager Tashkent", "Manager Samarkand", "Manager Tashkent without export"…
- Keep roles about job functions; move scope (branch, team, region) into attributes.
- Allow multiple roles per user instead of creating combined roles.
- Make rare permissions separate grants rather than new role variants.
- Review roles periodically: remove unused ones and merge near-duplicates.
- Show in the admin panel what each role actually allows, in plain language.
Example: admin panel of an online store
- Roles: Owner, Admin, Content manager, Support, Warehouse.
- Permissions:
products.edit,orders.view,orders.refund,users.manage,settings.payments. - Attribute rules: support can view orders but refund only up to a limit set in settings; warehouse sees only orders of its warehouse; nobody can change their own roles.