这个谁能按 — 把按钮藏起来不是权限
6 分钟门店 POS 上有个「撤销销售」按钮。只有店长该按。
一般是这么做的:如果登录的人是店员,就不画那个按钮。
然后管这叫权限处理。它不是。
不画它什么也拦不住
不画按钮拦住的只是**「按那个按钮把请求发出去」这一条路径。** 请求可以从别处发出。画面定义可以改,甚至完全不要画面直接调工具也行。画面是客户端,而客户端是别人的。
所以这个样例反着来。把按钮给两个人都看。

这是店员的画面。「Void last sale」和「Open drawer」都原样在那儿。按得下去。
按下去是这样。
[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)卖是卖成了,撤销没成。 不是画面拦住的,是服务器没做。
店长打开同一个画面

[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)同样的按钮、同样的顺序、不同的结果。
而且画面文件是同一个文件。 这正是这一篇的主张,所以测试装置把哈希打了出来。
screen ui/pages/register.json sha256:72288a2c655a — one file, both roles
the screen offers 3 buttons and shows all of them to everyone角色不是参数
这是这个样例里唯一的设计决定。
/// Who this session is. Fixed for its lifetime, never taken from a call.
final String role;假设 sale.void 把 role 当参数收。
sale.void { "role": "manager" }那么任何能调用它的人都可以自称是店长。 那一刻真正在拦着的,就只剩「画面不发这个值」,于是我们又回到了「不画按钮」。
所以角色在会话打开时就定下来。在这个样例里是启动参数,在真实部署里则是那条连接所认证到的身份。
final role = args
.firstWhere((a) => a.startsWith('--role='), orElse: () => '--role=staff')
.substring('--role='.length);画面不发送角色。就算想发也发不了。
验证守住这一点 — 画面定义里一旦出现角色名,就判失败。
# 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
fi画面显示 {{role}}。那是把服务器告诉它的东西画出来,它不决定任何事。
拒绝是一条记录
拒绝完就结束,那只是一半。
/// 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=staff只有被拒绝的人知道的拒绝,算不上什么管控。 得让当时不在场的人明天能读到,它才是管控。「今天有十二次试图打开钱箱」这个事实,即便那十二次全被挡住了,也是老板该知道的信息。
验证也直接去数那个文件。
AUDIT=$(wc -l < register_server/audit.log | tr -d ' ')
[ "$AUDIT" -eq 2 ] || { echo " audit log has $AUDIT lines, expected 2"; exit 1; }不把画面藏起来,体验不就差了吗
会差。按不了的按钮还摆在那儿,不是好界面。
可这两件事在不同的层。 藏起来没问题 — 只要别把「藏起来」叫作权限处理。
正确的顺序是这样。
| 层 | 做什么 | 没有它就 |
|---|---|---|
| 工具 | 拒绝并记录 | 谁都能干 |
| 画面 | 把按不了的变灰或隐藏 | 按一下才知道 |
没有上面那层,下面那层就是装饰。 没有下面那层,上面那层只是不体贴。不体贴和有窟窿之间,先修哪个是有定论的。
这个样例没做下面那层,就是这个缘故。为了单独把上面那层亮出来。
这个样例的范围
没有认证。 角色是通过启动参数进来的。现实中那个位置该是登录、令牌或证书,而那正是这个样例最大的空白。 它只展示了「角色挂在会话上」这个结构,并没有做「确认那个角色是不是真的」这件事。
只有两个角色。 真实门店会分成店员、店长、总部、结算负责人,而且不同工具的限制方式也不同。这里只有一个 _managerOnly 集合。
没有时间和金额条件。「50 美元以下店员也能撤销」这类规则在现实中常见得多,而我们没放进来。
画面这一侧的体贴也没做。 上面写着「藏起来没问题」,而这个样例并没有藏。是为了把主张亮出来才故意如此。
一句话
权限不是画面画了什么,而是工具做了什么。
重画一个画面只需改一个文件,而这在这个平台上尤其容易。容易正是问题。 把规则放在容易改的那一层,规则也就同样容易消失。
练习任务
连续发送两次相同的预订。证明第二次不会再生成一个预订,并说明是哪一方保证了这一点。