Who May Press This — Hiding a Button Is Not Authorisation
6 minA shop till has a "void sale" button. Only the manager should press it.
The usual approach: if the person logged in is an assistant, do not draw that button.
And that gets called authorisation. It is not.
Not drawing it stops nothing
Not drawing the button stops one path — the one where pressing that button sends the request. Requests can come from elsewhere. The screen definition can be edited, or the tool can be called with no screen at all. The screen is a client, and the client is somebody else's.
So this sample does the opposite. It shows the buttons to both of them.

This is the assistant's screen. "Void last sale" and "Open drawer" are both right there. They can be pressed.
Pressing them goes like this.
[staff] sale.ring -> "rang 3000" (sales=4 voided=0 refused=0)
[staff] sale.void -> "sale.void is not yours to press" (sales=4 voided=0 refused=1)
[staff] drawer.open -> "drawer.open is not yours to press"(sales=4 voided=0 refused=2)The sale went through and the void did not. The screen did not block it; the server did not do it.
The manager opens the same screen

[manager] sale.ring -> "rang 3000" (sales=4 voided=0 refused=0)
[manager] sale.void -> "voided 3000" (sales=3 voided=1 refused=0)
[manager] drawer.open -> "drawer opened" (sales=3 voided=1 refused=0)Same buttons, same order, different results.
And the screen file is the same file. That is this piece's claim, so the harness prints the hash.
screen ui/pages/register.json sha256:72288a2c655a — one file, both roles
the screen offers 3 buttons and shows all of them to everyoneA role is not an argument
This is the only design decision in this sample.
/// Who this session is. Fixed for its lifetime, never taken from a call.
final String role;Suppose sale.void took role as an argument.
sale.void { "role": "manager" }Then anybody who can call it can say they are the manager. At that moment the only thing genuinely stopping them is that the screen does not send that value, and we are back to "do not draw the button".
So the role is fixed when the session opens. In this sample by a launch argument; in a real deployment by whatever that connection authenticated as.
final role = args
.firstWhere((a) => a.startsWith('--role='), orElse: () => '--role=staff')
.substring('--role='.length);The screen does not send a role. It could not if it wanted to.
The verification protects that — if a role name appears in the screen definition, it fails.
# The screen may *display* the role it was told. It must never name one — the
# moment "manager" or "staff" appears in a screen definition, some branch is
# being decided in the wrong place.
if grep -qiE '"[^"]*(manager|staff)' register.mbd/ui/pages/register.json; then
echo " the screen definition names a role — the rule has leaked out of the tool"
exit 1
fiThe screen displays {{role}}. That is drawing what the server told it, and it decides nothing.
A refusal is a record
Refusing and stopping there is half of it.
/// Refusals go here as well as into the answer. A refusal that only the
/// person who was refused can see is not much of a control — the point of
/// writing it down is that somebody who was not standing there can read it
/// tomorrow.
final File audit;audit log holds 2 lines: refused sale.void for role=staff | refused drawer.open for role=staffA refusal only the refused person knows about is not much of a control. It becomes one when somebody who was not standing there can read it tomorrow. That there were twelve attempts to open the cash drawer in a day is information the owner needs, even if all twelve were blocked.
The verification counts the file directly.
AUDIT=$(wc -l < register_server/audit.log | tr -d ' ')
[ "$AUDIT" -eq 2 ] || { echo " audit log has $AUDIT lines, expected 2"; exit 1; }Doesn't not hiding it make the UX worse
It does. A button you cannot press being visible is not good UI.
But those are two different layers. Hide it, by all means — just do not call hiding it authorisation.
This order is the right one.
| Layer | What it does | Without it |
|---|---|---|
| Tool | Refuses and records | Anybody can do it |
| Screen | Dims or hides what cannot be pressed | You find out by pressing |
Without the top, the bottom is decoration. Without the bottom, the top is merely unfriendly. Between unfriendly and open, which to fix first is not a matter of taste.
That is why this sample did not do the bottom. To show the top alone.
The range of this sample
There is no authentication. The role arrives as a launch argument. In reality a login, a token or a certificate belongs in that place, and that is the biggest blank in this sample. It shows only the structure in which a role attaches to a session, not the work of checking whether that role is genuine.
There are only two roles. A real shop divides into assistant, manager, head office and settlement, with different tools gated differently. Here it is one _managerOnly set.
There are no time or amount conditions. Rules like "under $50 an assistant may void too" are far more common in reality, and are not in here.
We also did not do the screen-side courtesy. Having written above that hiding is fine, this sample does not hide. Deliberately, to show the claim.
One line
Authorisation is not what the screen draws; it is what the tool does.
Redrawing a screen means editing one file, and that is especially easy on this platform. Easy is the problem. Put the rule in the layer that is easy to change and the rule is that easy to remove.
Practice task
Send the same booking twice in a row. Show that the second one does not create a second booking, and explain which side made sure of it.