概述
字段声明为静态 readonly: true 时,该限制只在 UI 层生效,服务端不会从更新载荷里剥离该字段。因此任何能 update 该记录的用户,都可以用一次直接的 REST PATCH 写入这个字段。凡是依赖 readonly: true 来保护「审批状态 / 审批节点 / 金额」等字段完整性的对象,都会被绕过。
对比:readonlyWhen: <CEL> 是服务端强制的——命中条件的字段在更新时被静默剥离(HTTP 200,值不变)。两者不一致。
- 框架版本:15.0.0
- 受影响对象的共享模型:
public_read_write(默认 OWD)
影响(真机复现)
对象 sporadic_application(零星派工申请单)带 4 级审批流。approval_status、approval_stage 声明为 readonly: true;confirmed_total 是金额字段。
通过 Console UI + 同一登录会话内的浏览器 fetch(同一个已登录 worker 的 cookie,非管理员,isSystem=false)端到端复现:
- worker 在 Console UI 新建草稿。创建表单根本不渲染
approval_status / confirmed_total——UI 层看起来受保护,唯一合法动作是「提交审批」按钮。记录停在 draft。
- 在同一登录会话的浏览器里直接发:
PATCH /api/v1/data/sporadic_application/<id>
{ "approval_status": "approved", "approval_stage": "approved",
"confirmed_total": 99999 }
→ HTTP 200
- 刷新详情页:状态显示审批通过,流水线跳到终节点,
confirmed_total = 99,999.00,并冒出下游「派工」动作按钮。
最终效果:申请人自我批准、完全跳过 4 级审批,并自定结算金额。
系统性——在第二个对象上用同样手法坐实:assessment(考核单,3 级签核,assessment_status 同样是 readonly: true)——PATCH { assessment_status:'approved', approval_stage:'approved' } → 200,dept_head → gm → finance 三级全跳过。凡是依赖 readonly: true 保护审批 / 状态 / 金额 / 判定字段的对象,一律中招。
补充:RECORD_LOCKED 只在审批流**活动态(pending)**时拦直接 PATCH。记录仍在 draft(流从未启动)时不会被锁,而 readonly: true 又只是 UI 层——所以 draft → approved 是一步直跳。
期望行为
readonly: true 的字段级写入应当服务端强制,和 readonlyWhen 字段在更新时被剥离的方式一致(insert 豁免)。即:对非特权写入者,静态 readonly: true 字段应在更新载荷里被静默丢弃。
线索 / 指引
readonlyWhen 已有服务端剥离路径(在 beforeUpdate 之后、基于合并的 {...previous, ...data} 求值,insert 豁免)。不一致点在于:静态 readonly: true 没有走同一条剥离路径。
- 我们应用侧的临时绕行:把
readonly: true 改成 readonlyWhen,或加一个高优先级 beforeUpdate 守卫,在 !ctx.isSystem 时剥离这些受保护字段。
概述
字段声明为静态
readonly: true时,该限制只在 UI 层生效,服务端不会从更新载荷里剥离该字段。因此任何能update该记录的用户,都可以用一次直接的 RESTPATCH写入这个字段。凡是依赖readonly: true来保护「审批状态 / 审批节点 / 金额」等字段完整性的对象,都会被绕过。对比:
readonlyWhen: <CEL>是服务端强制的——命中条件的字段在更新时被静默剥离(HTTP 200,值不变)。两者不一致。public_read_write(默认 OWD)影响(真机复现)
对象
sporadic_application(零星派工申请单)带 4 级审批流。approval_status、approval_stage声明为readonly: true;confirmed_total是金额字段。通过 Console UI + 同一登录会话内的浏览器 fetch(同一个已登录 worker 的 cookie,非管理员,
isSystem=false)端到端复现:approval_status/confirmed_total——UI 层看起来受保护,唯一合法动作是「提交审批」按钮。记录停在draft。confirmed_total = 99,999.00,并冒出下游「派工」动作按钮。最终效果:申请人自我批准、完全跳过 4 级审批,并自定结算金额。
系统性——在第二个对象上用同样手法坐实:
assessment(考核单,3 级签核,assessment_status同样是readonly: true)——PATCH { assessment_status:'approved', approval_stage:'approved' }→ 200,dept_head → gm → finance 三级全跳过。凡是依赖readonly: true保护审批 / 状态 / 金额 / 判定字段的对象,一律中招。补充:
RECORD_LOCKED只在审批流**活动态(pending)**时拦直接 PATCH。记录仍在draft(流从未启动)时不会被锁,而readonly: true又只是 UI 层——所以 draft → approved 是一步直跳。期望行为
readonly: true的字段级写入应当服务端强制,和readonlyWhen字段在更新时被剥离的方式一致(insert 豁免)。即:对非特权写入者,静态readonly: true字段应在更新载荷里被静默丢弃。线索 / 指引
readonlyWhen已有服务端剥离路径(在beforeUpdate之后、基于合并的{...previous, ...data}求值,insert 豁免)。不一致点在于:静态readonly: true没有走同一条剥离路径。readonly: true改成readonlyWhen,或加一个高优先级beforeUpdate守卫,在!ctx.isSystem时剥离这些受保护字段。