Infisical added folder-level access control to a permissions system that already had too many ways to grant access. The write-up is framed around a claim that generalizes well past authorization: RBAC belongs to the same class of work as billing, schema migrations, and audit logs — nobody praises a correct one, and everyone notices when it is wrong.

Why roles alone could not express the requirement:

  • An engineer who is the only one with Netsuite experience needs their engineer role plus specific credentials. A contractor refactoring Lambda functions needs that role minus billing data. The role does not change; the access requirement does.
  • Both conventional fixes are bad — editing the shared role grants or removes too much, and cloning a custom role creates parallel configuration for a one-off case that will drift.
  • The legacy workaround, per-person Additional Privileges, could not subtract privileges and stored paths as strings, so renaming a folder silently broke a grant. Folder grants key off folder IDs instead.

They deliberately skipped Zanzibar-style authorization (SpiceDB, OpenFGA):

  • It would mean rewriting every existing permission check.
  • Self-hosted customers would have to run another stateful service and absorb breaking changes — and an open-source project cannot deprecate an access model, because some instance somewhere still depends on it.

The implementation is two moves worth stealing:

  • Five tiers standardize what a grant means: List, Read, Edit, Manage, Full Access.
  • Folder access is appended as a second CASL layer — deny every action on that folder, then allow back only what the tier permits. The deny block is identical for all tiers, so no tier can forget an exclusion, and a test fails if anyone adds a new path-scoped permission without extending it.

The cache-invalidation section is the most honest part of the post. Permission checks run on every request, so results are cached and validated by fingerprint:

  • Fingerprinting the grant rows failed, because rules need a path resolved at compile time — every rename recompiled everything.
  • Fingerprinting the folders grants point at failed too, because a path is derived from the ancestor chain: renaming /a changes what a grant on /a/b resolves to while /a/b itself is untouched.
  • What shipped is coarser than either: a per-project version counter bumped on every rename, move, and delete, read into the fingerprint instead of the cache key.

The transferable lesson is the layering. Instead of teaching every tier to deny what it must not grant, one identical deny block does the denying and the tier only allows a subset back — a structure that makes the security invariant hard to break by accident, and testable when someone tries. The constraints are also stated plainly: this is another layer on an access system that already had several, so the simplicity is user-facing while the internal model got more complex. That is usually the right trade for infrastructure nobody is supposed to think about.