Now-Next

SaaS architecture

Permissions in SaaS: roles, not checkboxes

The third irreversible decision: who may do what. Why a list of checkboxes per user seizes up after twenty customers, and what does work instead.

Chris van Eijk · · 8 min read

This is the third and last of the decisions you will almost never straighten out later in a SaaS product. The first was about how you keep customers apart, the second about how you calculate money. This one is about who may do what, and it nearly always seizes up the same way.

How it starts

The first customer asks whether an employee can see invoices but not salaries. That is a reasonable request and it has a reasonable answer: a checkbox on the user profile.

The second customer wants the same thing the other way round. The third wants it per department. By the tenth there are forty checkboxes on a screen nobody dares touch anymore, and by the twentieth the most frequent support question has become: which boxes do I tick to let this person do what they were already doing yesterday?

That is the moment it is too late, and it is too late for three reasons at once:

  • It cannot be tested. Forty independent checkboxes are more combinations than fit in your test suite. In practice you test five, and the rest exist only at customers.
  • It cannot be explained. A customer can no longer describe what “bookkeeper” means at their company, because it is not a concept anymore but a collection of accidents.
  • It cannot be migrated. Add a feature and you have to guess, for every existing user, whether they may use it. Every answer is wrong for somebody.

What does work

Permissions belong to a role, and a user is given a role. Not the other way around.

Concretely that means:

There is a fixed, small set of permissions. They are named after what someone can doinvoice.create, ledger.read — and not after the screen they sit on. Screens move; actions do not.

Roles are compositions of those permissions, and there are few of them. If twenty roles appear, they are not roles anymore but checkboxes under another name.

A customer may compose their own role, but only from the fixed set. That is the flexibility customers actually ask for, and it is the only form you can still test: the number of permissions stays finite, so the matrix stays finite.

The difference from checkboxes looks cosmetic and is not. With checkboxes, the answer to “may this person do this?” is a property of the person. With roles it is a property of the system, and that is the difference between something you can look up and something you have to reconstruct.

Permissions are not scope

This is the mistake made even by people who get the above right.

A permission says what somebody may do. It does not say over what. The second is scope, and it belongs with customer separation — with the tenant, and sometimes with a layer above it.

At booxx there is an accounting firm in between: a firm sees its own clients, a client sees only itself. That is not a permission but a boundary, which is why it does not live in the roles table but in the database, as a policy on the row. Model it as a permission — may_see_clients_of_firm — and the separation between two companies hangs on an if in the application, which is the same mistake as a tenant filter without a second lock.

The rule of thumb: the role decides what, the boundary decides where, and the two must never stand in for each other.

Four rules that keep it upright

Deny by default. An action without an explicit permission is forbidden, not allowed. A new endpoint nobody has attached to a role should be unreachable — annoying while building, and wrong in the right direction.

Check on the server, always. Hiding a button is courtesy, not security. The check belongs where the action actually happens, and there is exactly one such place per action.

Record who changed the role. A role change is the one event that explains after the fact why somebody could reach something. Without that trail every incident investigation is guesswork. It is also the cheapest thing to do up front and the most expensive to reconstruct, because history you did not store does not come back.

One place the answer comes from. As soon as there are two functions that both answer “may this person do this”, they will drift apart. Usually within a month, and always quietly.

The test that matters

The most useful test here is a matrix: for every role, for every endpoint, allowed or not allowed. It is boring to write and it is the best money you will ever put into a test suite.

There is one trick in it that makes the difference. Have the test pull the list of endpoints from the application itself rather than typing it out. Then it fails automatically the moment somebody adds an endpoint without thinking about it. A matrix maintained by hand covers exactly last year’s code.

It is also precisely the kind of work that suits the nightly crew of agents that fills in our coverage: it is fully derivable from the code, it is repetitive, and that is exactly why people keep putting it off.

What it costs to do later

Up front: a permissions table, a roles table, a link between them, and one function that answers the question. A day, maybe two.

Afterwards: you have to infer, for every existing user, which role they actually had. The only way is to interpret their checkboxes, and those were set over the years by different people for different reasons. Every inference you make gives somebody more or fewer rights than yesterday — and you find out when it goes wrong, because nobody calls the helpdesk to report that they can do too much.

That was the list

Customer separation, money, permissions. Everything outside those three you are allowed to change later, and that is exactly why these three deserve the attention they rarely get.

Would you like us to build this for you? That is what we do — and for an existing product, scaling it up is usually where this conversation starts.

Let's talk

What are we building?

One conversation is enough to know whether we fit. Tell us what you have in mind — we will tell you how fast it can happen, and whether we are the right people for it.

Start the conversation