Every operational system I have been handed has a version of the same story in it. Somebody needs to cover the billing queue for two weeks. Building a proper role for that would take a sprint nobody has, so they get handed access that already works, which is usually the controller's. The coverage ends. The access does not. Months later a credit goes out that nobody can account for, and the honest answer to "who could have done this" is a number in the dozens.
Nothing careless happened there. Every step was the reasonable move in front of a person with a queue to clear. That is what makes this worth designing on purpose, before the build, rather than discovering it during an audit or after a bad week.
Two levels is not a permission model
Most internal tools, including many custom ones, start with two settings: admin and everyone else. It works for a while because the first users are the people who commissioned the system.
Then the finance lead needs to correct a posted invoice, which only admins can do, so she becomes an admin. Then the new operations coordinator needs the same queue, so he becomes an admin. Then support needs to resend a receipt. Within a year, admin is not a role. It is a list of people who once needed something.
The tell is easy to check. Ask how many accounts can issue a refund, waive a fee, change a bank account on file, or export the full contact list. If the person who runs the system has to go look, and then looks surprised, the model already drifted.
The three questions that make a role
When I work through permissions with a client, I do not start from job titles or from the software's built-in roles. I start from the actions that carry consequence, and I ask three questions about each one.
- Can they see it?
- Visibility is its own decision. A support agent may need the donation history to answer a question, and may have no business seeing the donor's employer, giving capacity note, or full card record.
- Can they do it?
- The write side. Issuing the refund, approving the grant disbursement, changing the payout account, closing the fund.
- Can they do it alone?
- The one most systems skip. Some actions should be possible for a role to start and impossible for that role to finish without a second person.
That third question is where the real protection lives, and it is the cheapest of the three to build if you decide on it early. Retrofitting a second approver into a workflow that assumed one actor is a rewrite of that workflow. Deciding up front that disbursements above a threshold need a second pair of eyes is a field, a queue, and a rule.
It also protects the people doing the work. A coordinator who cannot single-handedly move money is a coordinator nobody can pressure into moving money, and who cannot be blamed when something goes wrong at three in the afternoon on a Friday.
Roles belong to jobs, exceptions belong to records
The rule that keeps a model from rotting is short: a role describes a job, not a person. If you cannot name a role without naming an individual, it is not a role yet. "Refund approver" is a role. "Whatever Dana needs" is a standing exception wearing a role's clothes.
Real work still produces exceptions. Somebody has to handle the escalation while the approver is on leave. The way to keep that from becoming permanent is to attach the exception to the record and the moment, not to the account. A one time override, with a reason typed in, an expiration, and a line in the history saying who granted it. Access that expires on its own is the single highest value feature in a permission model, because nobody has ever reliably remembered to take access away.
Reading is an action too
Teams tend to argue hard about who can approve things and wave through who can look at things. In regulated and donor facing operations, the reading side is usually the larger exposure.
Export is the one to watch closely. Once a spreadsheet of constituents leaves the system, every control you built stops applying to it. It lives in an inbox, then in a downloads folder, then in a personal drive after somebody leaves. If exporting is its own permission, logged with who and when and what filter was applied, you can still answer questions about it later. If export is a button everyone has, you cannot.
A model you could actually write down
Here is the shape of the thing I am asking for. Four or five rows, plain language, agreed by the people who run the work, before anyone opens an editor.
| Role | Can see | Can do | Needs a second person |
|---|---|---|---|
| Support | Account, transaction history, status | Resend receipt, update contact details | Any refund |
| Billing coordinator | Everything support sees, plus payment method on file | Refund up to the stated limit, correct a coding error | Refund above the limit, fee waiver |
| Controller | All financial records, all exports | Approve refunds and waivers, close a period | Change a payout bank account |
| Executive | All records, reporting views | Read only in the operational system | Not applicable |
Two things about that table are deliberate. The executive row is read only, because leadership almost never needs to transact and the account is the most valuable one to compromise. And the controller, the most trusted person in the list, still cannot change a bank account by herself. Controls that only constrain junior staff are not controls. They are an org chart.
What you get for the trouble
This work never feels urgent, which is why it gets deferred. What it buys is specific.
- Offboarding becomes one change. Somebody leaves, you remove them from a role, and you are done. Without roles, offboarding is an archaeology project across a dozen screens, and the parts you miss are invisible.
- The audit question becomes answerable. "Who could have done this, and who did" is a question about roles and history. If the answer requires a database query and a guess, you are going to spend a week on it and still hedge.
- A mistake stays small. Most incidents I have seen were not malice. Someone with more access than their job required clicked the wrong row. Scope limits damage from ordinary human error more often than it stops anyone dishonest.
- You can staff flexibly. Temporary coverage stops being a security decision. You add someone to a role for a month and it expires.
What to decide before anyone writes code
You do not need a security consultant for this. You need an afternoon with the people who do the work, and answers to five things.
- Which actions in this system move money, change who gets paid, or expose personal data. List them by name.
- For each one, the three questions: see, do, do alone.
- The thresholds. Not "large refunds need approval," but the number, and who owns changing that number later.
- Who reviews the access list, and how often. Quarterly is fine. Never is what usually happens.
- What happens to an account the day someone leaves, and who is responsible for making it happen.
Write the answers in plain sentences. That document is a specification, and a good build team will turn it into the system's permission model without much translation. Skipping it does not save the work. It moves the work to the point where it has to be done live, in a system people already depend on, usually right after something has gone wrong.
If you are scoping an internal system and you are not sure who should be able to do what inside it, that is exactly the kind of thing we map in discovery. It is a short, structured engagement that works out how the operation really runs, and what the system has to enforce, before anyone writes code. You can start that conversation by clicking on the button below. Start with Discovery