The Three Permissions That Aren't CRUD
View, create, edit, delete times every QA entity gets you most of a permissions model. Three responsibilities refused to fit that grid — and each one names a risk the grid can't see.
Here’s a requirement that sounds trivial: a tester should be able to run tests without being able to reshape the run.
Try expressing it as view, create, edit or delete. Recording a result is obviously an edit — it writes to the run. But so is adding twelve test cases to the run, removing four, and changing what it covers. Grant edit and you’ve granted both. Withhold it and your testers can’t record results.
The grid can’t say it. That’s not a flaw in this particular grid; it’s what happens when you model what an action touches instead of what an action means.
Verb times entity gets you most of the way
To be clear, the grid is most of the model and it’s fine. Every QA entity — test cases, suites, releases, executions, requirements, defects, boards, shared steps, parameters, custom fields, reports, access tokens — carries view, create, edit and delete, at both workspace and project level.
- Run tests Recording a result is not reshaping the run
- Trigger automation Starts a real build on your own infrastructure
- Manage boards Your own board is granted by ownership, not permission
It’s predictable, it’s explainable to a customer, and a new entity slots in without a design discussion. Ninety percent of what anyone wants to express, it expresses.
The interesting part is the ten percent, because each exception marks a place where the risk isn’t shaped like the data.
Three that refused to fit
Running tests is not editing runs. So recording results has its own permission, covering everything a tester does while working through a run — final status, step status, evidence, notes, time spent, applying a refreshed test case. A pure tester role gets all of that and none of the ability to change which cases are in the run, what it covers, or when it completes. Two genuinely different jobs that happen to change the same record.
Triggering a build is not creating a run. Starting an automated run kicks off a real build on your own build pipeline. That’s not a data change with a bigger number attached — the blast radius leaves the product entirely. It can use up build minutes, deploy to an environment, page someone. So triggering is its own permission, deliberately separated from creating the run that will hold the results. Choosing which pipeline a run uses is part of editing the run. Pulling the trigger is not.
Owning a board is not a permission at all. Defect boards have a manage permission, but editing your own board doesn’t need it — you get that by having created the board. The permission is for reaching into boards you don’t own. And board permissions deliberately don’t cover the cards: dragging a card changes the defect, so it still needs permission to edit defects. Hand someone a board without that and they can look but not drag.
Refuse by default, in a specific order
The permissions are only half of it. The order Hawzu checks them in is the other half, and it encodes three arguments.
Hawzu’s own recovery access is checked first, before anything looks up who belongs to your workspace. Not for speed — so that recovery access never depends on the part of the system most likely to be broken when you need it.
Whether the project can still be reached is checked before the owner’s free pass. A workspace owner passes almost everything, but not this: if the project has been deleted, it can’t be reached, no matter who’s asking. Checking the owner first would let an owner keep using a project they just deleted, which would make deleting a project cosmetic rather than real.
Anything without a permission assigned is refused. Every action in Hawzu is listed against the permission it needs, and an action missing from that list is refused rather than allowed. This is the decision that matters most in practice. If the default were to allow, forgetting to list a new action would ship it wide open, and nothing would tell you. Refusing by default means the mistake shows up as a refusal the first time anyone tries the action, and gets fixed the same hour.
The list that isn’t the contract
One honest wrinkle, because it’s the kind of thing that makes a permissions model quietly wrong.
Hawzu keeps a master list of permission names. It reads like the authoritative definition. It isn’t. What’s actually enforced is the separate record of which action needs which permission — and the two have drifted. The run-tests permission covers the whole of recording results and is offered in the role builder, and it isn’t on the master list at all.
Nothing is broken. The permission works, because the record of what each action needs is what’s enforced. But a list that looks authoritative and enforces nothing is worse than no list, because the next person to read it will trust it.
Documentation that looks like a contract, and isn’t enforced as one, is a trap with a helpful tone of voice.
The lesson generalises past permissions: if two lists describe the same rule and only one is enforced, the other will drift, and it will drift silently. Either generate it from the enforced one or delete it.
Where this stops being abstract is a cycle run by people who don’t work in the tool every day — handing business testers a narrow role for UAT is the case that exercises all three of these at once.
Which is really the same principle as everything above. A permission model isn’t a taxonomy of your data — it’s a description of who is trusted with which consequences, and the consequences don’t always line up with the tables.
Model what an action means, not what it touches.
Building a custom role