RBAC is harder than it looks
Role-based access control sounds simple until you have multi-tenant data, sensitive documents, and five modules that all have to agree.
Key takeaways
- RBAC is a data isolation problem as much as a permissions problem
- Every query needs a tenant boundary, not just every endpoint
- Least privilege has to be enforceable, not aspirational
Background
On an ERP platform handling Finance and Payroll documents, "who can see this file?" turns out to be the wrong question. The real question is "who can see this file, for this company, in this module?"
The problem
A naive permission check looks like this:
def can_view(user, document):
return user.role in ("admin", "finance")
That check is blind to tenancy. If the document lookup doesn't also scope by company_id, a Finance user from company A can read company B's payroll.
How it works
Scope the query, not just the endpoint:
select d.*
from documents d
join users u on u.id = $1
where d.company_id = u.company_id
and d.module in (select module from role_permissions where role = u.role);
Then layer rate limiting and access-scoped sharing on top so a leaked link can't outlive its grant.
What I learned
- Enforce isolation at the data layer — one missing
whereclause is a breach - Write tests that attempt cross-tenant access; they catch regressions cheaply
- Model permissions as data, so changing them isn't a deploy
Spotted something wrong, or want to talk security? Reach me at tpjn02@gmail.com or try my AI twin.