The Hasan Dispatch logo

The Hasan Dispatch

Archives
Log in
Subscribe
August 5, 2022

Designing a Permission System for 8 Companies, 12 Departments

Multi-tenant RBAC with inheritance, overrides, and least-privilege enforcement. The first version was too simple, the second too complex, the third just right.

#Architecture #RBAC #Security

The Requirement

The system needed to support eight companies, twelve departments, and several hundred users, each with access requirements that didn't map cleanly onto a single hierarchy. Every company operates its own admin, some employees need access across multiple companies, and some roles are global by nature (IT administrator) while others are strictly department-specific (production line manager). The first design challenge was enumerating all of these patterns; the second was modeling them in a way that would scale as new patterns emerged. The naive approach — a single users table with a flat role column — breaks down immediately, because it can't express "editor in Company A but viewer in Company B," let alone permission inheritance or scoped overrides.

What the system actually needed was multi-tenant role-based access control with proper inheritance, the ability to override inherited permissions at a lower level, and consistent enforcement of least-privilege as the default posture. "Least-privilege as the default" is a critical design principle: when a new user is created or a new resource is added, the system grants no permissions by default, and access must be explicitly granted rather than explicitly revoked. A system that defaults to broad access accumulates over-permissioned users over time as people change roles and old permissions are never cleaned up; a system that defaults to no access forces explicit, intentional grants that can be reviewed and audited.

The Three Versions

The first version we built had three roles per tenant (admin, editor, viewer) and was unusable within a month because real organizations need finer-grained control. The second version had a fully general attribute-based access control model where permissions could be defined as arbitrary boolean expressions over user and resource attributes, which was theoretically correct but practically impossible to administer — no admin could write a correct policy without engineering support. The third version — a flat permission map organized by role grouping, with tenant scoping applied consistently at the point of assignment — was the one that actually held up. It was expressive enough for every real-world case, simple enough for admins to configure, and consistent enough for engineers to extend.

The "write the third version" pattern shows up in almost every non-trivial system: the first version is too simple because you don't yet know the requirements, the second is too complex because you overcorrect based on the requirements you missed, and the third is right because it incorporates the lessons from both failures. The cost of the first two versions wasn't wasted — it was the tuition for learning what the system actually needed to be — but the lesson is to get through the versions quickly rather than polishing a version that's fundamentally wrong. Prototyping and discarding is faster than building and maintaining the wrong thing.

Defense in Depth

On the frontend, permissions are enforced at three separate layers as a defense-in-depth measure: route guards prevent navigation to a page the user doesn't have access to, component-level guards hide or disable individual UI elements the user isn't permitted to use, and API-level guards on the backend catch any attempt to bypass the UI entirely. Relying on any single layer alone would have left a gap; together they made the permission boundary consistent regardless of how a user tried to interact with the system. The frontend layers exist for user experience (a user shouldn't see a button they can't use), while the backend layer exists for security (the frontend can be bypassed by anyone with a network tab open).

The other lesson worth calling out: design the audit trail from day one, not after the fact. Retrofitting audit logging onto a permission system that's already in production is painful, and it's nearly impossible to backfill history for changes that happened before the logging existed. Audit logs are one of those features nobody asks for until something goes wrong, at which point they're the most important feature in the system. Building them up front means you have them when you need them, rather than having to explain to leadership why you can't answer "who changed this user's permissions last Tuesday."

Don't miss what's next. Subscribe to The Hasan Dispatch:
Share this email:
Share on LinkedIn
GitHub
🙃
LinkedIn
Powered by Buttondown, the easiest way to start and grow your newsletter.