现象
对象列表页,每一行右侧「更多操作」溢出菜单里的内建【编辑】【删除】 ,对没有 allowEdit / allowDelete 的账号照常渲染 。
点【编辑】能打开字段全部预填的编辑对话框,直到点【更新】才由服务端拒掉。服务端门禁是硬的(见下「服务端是对的」一节),所以这不是越权写入,但:
「删除」这种破坏性入口摆在无权账号面前,误点成本高;
用户被引导走进一个注定失败的表单;
同一个页面里另外两处权限门是对的 ,所以这更像「做漏了一处」而不是「还没做」。
实测于 @objectstack/*@17.0.0-rc.5,zh-CN Console,一个下游业务应用的自建对象上。
根因:这一处的门用了 apiOperations,而它与用户无关
同一份 GET /api/v1/auth/me/permissions 响应里,同一个对象:
账号
allowCreate
allowEdit
allowDelete
apiOperations
A(对该对象零写权)
false
false
false
get,list,create,update,delete,upsert,bulk,aggregate,history,search,import
B(对该对象有 update 权)
false
true
false
完全相同的 11 个 verb
C(只读)
false
false
false
完全相同的 11 个 verb
量化判据 :把 A 与 B 的响应逐对象 比对,30 个共有对象的 apiOperations 100% 相同(差异 0 个) ,而 allowEdit 明确不同。
这与 spec 对该字段的定义一致:
Server-resolved effective API operations for this object … Present only when the object tightens exposure via apiMethods
即 apiOperations 是对象的 API 暴露面 (「这个对象开放了哪些 verb」),不是 「当前用户能做哪些操作」。拿它与行动作求交 ⇒ 恒 fail-open 。
同页三处门的对照(两对一错)
界面位置
用的判据
结果
列表工具栏【新建】/【导入】
can(obj, 'create') → allowCreate
✅ 正确隐藏
记录详情页头【编辑】/【删除】
逐记录 update/delete 权限判定
✅ 正确隐藏
列表行「更多操作」内建【编辑】【删除】
只与 apiOperations 求交
❌ 恒可见
复现
任一对象,准备两个账号:X 的权限集给该对象 allowEdit/allowDelete = false(只读),Y 给 allowEdit = true;
两个账号分别打开该对象的列表页;
展开任意一行右侧的「更多操作」溢出菜单。
实际 :X 与 Y 看到的菜单项完全相同,都含【编辑】【删除】。
期望 :X 看不到【编辑】【删除】—— 与同页工具栏【新建】按 allowCreate 隐藏的口径一致。
对照观察:同一个 X 账号,列表工具栏的【新建】【导入】确实 是隐藏的,详情页头的【编辑】【删除】确实 也是隐藏的。三处判据不统一。
服务端是对的(未越权,纯 UI 门缺失)
动作
账号
HTTP
响应
DB
PATCH /api/v1/data/<obj>/{id}
X
403
PERMISSION_DENIED: operation 'update' … not permitted
未改写
DELETE /api/v1/data/<obj>/{id}
X
403
PERMISSION_DENIED: operation 'delete' …
记录仍在
下游能不能自己绕过去?——实测:不能
在 rc.5 上做过真实探针(验完已回滚):
userActions: {
edit : { visibleWhen : P `os.user.email == "someone@example.test"` } ,
delete : { visibleWhen : P `record.status == "disabled"` } ,
}
结论三条:
✅ 内建行 Edit/Delete 确实吃 userActions.edit/delete.visibleWhen —— 行动作声明 visible: false 仍然渲染该菜单项——可见性门用真值判断(#3492 在行菜单上的未收口部分) #3758 的「声明即生效」门(visibleWhen != null)在 rc.5 里是通的,通道存在;
✅ 谓词上下文里有 os.user,rc.5 实测作用域为 { current_user, user, ctx.user, os.user, app, data, features };
❌ 但 os.user 里没有任何权限信息 。三个不同授权的账号,会话 user 对象逐字相同 :
{ "role" : " user" , "positions" : [" user" , " org_member" ], "isPlatformAdmin" : false }
roles 键不存在;positions 恒为 ["user","org_member"](该项目的权限集直绑用户,不走挂岗)。
⇒ CEL 表达不出 「当前用户对本对象有无写权」。唯一凑得出来的写法是把账号或角色名硬编码进对象元数据 ,等于把权限模型抄第二份、必然漂移 —— 不是可接受的解。
期望的修法
列表行内建【编辑】【删除】的可见性,改用与工具栏同一份有效权限 判据(allowEdit / allowDelete,以及既有的逐记录权限钩子),而不是 apiOperations。
apiOperations 作为「对象暴露面」仍可继续参与判定(暴露面没开 delete 时当然也不该显示),但它不能单独充当用户权限门 。
相关
若认为 os.user 该带上权限/角色信息(那样下游至少有自救通道),可另开一条;本条只针对「内建行动作没接权限门」。
现象
对象列表页,每一行右侧「更多操作」溢出菜单里的内建【编辑】【删除】,对没有
allowEdit/allowDelete的账号照常渲染。点【编辑】能打开字段全部预填的编辑对话框,直到点【更新】才由服务端拒掉。服务端门禁是硬的(见下「服务端是对的」一节),所以这不是越权写入,但:
实测于
@objectstack/*@17.0.0-rc.5,zh-CN Console,一个下游业务应用的自建对象上。根因:这一处的门用了
apiOperations,而它与用户无关同一份
GET /api/v1/auth/me/permissions响应里,同一个对象:allowCreateallowEditallowDeleteapiOperationsget,list,create,update,delete,upsert,bulk,aggregate,history,search,import量化判据:把 A 与 B 的响应逐对象比对,30 个共有对象的
apiOperations100% 相同(差异 0 个),而allowEdit明确不同。这与 spec 对该字段的定义一致:
即
apiOperations是对象的 API 暴露面(「这个对象开放了哪些 verb」),不是「当前用户能做哪些操作」。拿它与行动作求交 ⇒ 恒 fail-open。同页三处门的对照(两对一错)
can(obj, 'create')→allowCreateapiOperations求交复现
allowEdit/allowDelete = false(只读),Y 给allowEdit = true;实际:X 与 Y 看到的菜单项完全相同,都含【编辑】【删除】。
期望:X 看不到【编辑】【删除】—— 与同页工具栏【新建】按
allowCreate隐藏的口径一致。对照观察:同一个 X 账号,列表工具栏的【新建】【导入】确实是隐藏的,详情页头的【编辑】【删除】确实也是隐藏的。三处判据不统一。
服务端是对的(未越权,纯 UI 门缺失)
PATCH /api/v1/data/<obj>/{id}PERMISSION_DENIED: operation 'update' … not permittedDELETE /api/v1/data/<obj>/{id}PERMISSION_DENIED: operation 'delete' …下游能不能自己绕过去?——实测:不能
在 rc.5 上做过真实探针(验完已回滚):
结论三条:
✅ 内建行 Edit/Delete 确实吃
userActions.edit/delete.visibleWhen—— 行动作声明visible: false仍然渲染该菜单项——可见性门用真值判断(#3492 在行菜单上的未收口部分) #3758 的「声明即生效」门(visibleWhen != null)在 rc.5 里是通的,通道存在;✅ 谓词上下文里有
os.user,rc.5 实测作用域为{ current_user, user, ctx.user, os.user, app, data, features };❌ 但
os.user里没有任何权限信息。三个不同授权的账号,会话 user 对象逐字相同:{ "role": "user", "positions": ["user", "org_member"], "isPlatformAdmin": false }roles键不存在;positions恒为["user","org_member"](该项目的权限集直绑用户,不走挂岗)。⇒ CEL 表达不出「当前用户对本对象有无写权」。唯一凑得出来的写法是把账号或角色名硬编码进对象元数据,等于把权限模型抄第二份、必然漂移 —— 不是可接受的解。
期望的修法
列表行内建【编辑】【删除】的可见性,改用与工具栏同一份有效权限判据(
allowEdit/allowDelete,以及既有的逐记录权限钩子),而不是apiOperations。apiOperations作为「对象暴露面」仍可继续参与判定(暴露面没开delete时当然也不该显示),但它不能单独充当用户权限门。相关
visible: false仍然渲染该菜单项——可见性门用真值判断(#3492 在行菜单上的未收口部分) #3758 行动作声明visible: false仍渲染(可见性门用真值判断)—— 不同:那条是「声明的门失效」,本条是「压根没接权限门」;requiredPermissions—— 同一族(动作面权限门未收口)的另一处,已修;visiblepredicate is a constanttrueon the bulk selection bar #3067 行级visible谓词在批量选择条上恒为true。若认为
os.user该带上权限/角色信息(那样下游至少有自救通道),可另开一条;本条只针对「内建行动作没接权限门」。